class IMAPProcessor
IMAPProcessor
is a client for processing messages on an IMAP server.
Subclasses need to provide:
-
A
process_args
class method that adds any extra options to the defaultIMAPProcessor
options. -
An initialize method that connects to an IMAP server and sets the @imap instance variable
-
A run method that uses the IMAP connection to process messages.
Reference:
email: http://www.ietf.org/rfc/rfc0822.txt imap: http://www.ietf.org/rfc/rfc3501.txt
Constants
- NEWER
- VERSION
-
The version of
IMAPProcessor
you are using
Attributes
Options Hash
from process_args
Was –verbose used?
Public Class Methods
Source
# File lib/imap_processor.rb, line 86 def self.add_move @@options[:MoveTo] = nil @@extra_options << proc do |opts, options| opts.on( "--move=MAILBOX", "Mailbox to move message to", "Default: #{options[:MoveTo].inspect}", "Options file name: :MoveTo") do |mailbox| options[:MoveTo] = mailbox end end end
Adds a –move option to the option parser which stores the destination mailbox in the MoveTo option. Call this from a subclass’ process_args
method.
Source
# File lib/imap_processor.rb, line 352 def initialize(options) @options = options @verbose = options[:Verbose] @boxes = options[:Boxes] Net::IMAP.debug = options[:Debug] end
Handles the basic settings from options
including verbosity, mailboxes to process, and Net::IMAP::debug
Source
# File lib/imap_processor.rb, line 126 def self.process_args(processor_file, args, required_options = {}) # :yield: OptionParser @@opts_file_name = File.basename processor_file, '.rb' @@opts_file_name = "imap_#{@@opts_file_name}" unless @@opts_file_name =~ /^imap_/ opts_file = File.expand_path "~/.#{@@opts_file_name}" if required_options then required_options.each do |option, (default, message)| raise ArgumentError, "required_options message is missing for #{option}" if default.nil? and message.nil? end end defaults = [{}] if File.exist? opts_file then unless File.stat(opts_file).mode & 077 == 0 then $stderr.puts "WARNING! #{opts_file} is group/other readable or writable!" $stderr.puts "WARNING! I'm not doing a thing until you fix it!" exit 1 end defaults = Array(YAML.load_file(opts_file)) end defaults.map { |default| options = default.merge @@options.dup options[:SSL] = true unless options.key? :SSL options[:Username] ||= ENV['USER'] options[:Root] ||= nil options[:Verbose] ||= false options[:Debug] ||= false required_options.each do |k,(v,_)| options[k] ||= v end op = OptionParser.new do |opts| opts.program_name = File.basename $0 opts.banner = "Usage: #{opts.program_name} [options]\n\n" opts.separator '' opts.separator 'Connection options:' opts.on_tail("-h", "--help", "Show this message") do puts opts exit end opts.on("-H", "--host HOST", "IMAP server host", "Default: #{options[:Host].inspect}", "Options file name: :Host") do |host| options[:Host] = host end opts.on("-P", "--port PORT", "IMAP server port", "Default: The correct port SSL/non-SSL mode", "Options file name: :Port") do |port| options[:Port] = port end opts.on("-s", "--[no-]ssl", "Use SSL for IMAP connection", "Default: #{options[:SSL].inspect}", "Options file name: :SSL") do |ssl| options[:SSL] = ssl end opts.on( "--[no-]debug", "Display Net::IMAP debugging info", "Default: #{options[:Debug].inspect}", "Options file name: :Debug") do |debug| options[:Debug] = debug end opts.separator '' opts.separator 'Login options:' opts.on("-u", "--username USERNAME", "IMAP username", "Default: #{options[:Username].inspect}", "Options file name: :Username") do |username| options[:Username] = username end opts.on("-p", "--password PASSWORD", "IMAP password", "Default: Read from ~/.#{@@opts_file_name}", "Options file name: :Password") do |password| options[:Password] = password end auth_class = NEWER ? Net::IMAP::SASL : Net::IMAP authenticators = auth_class.authenticators auth_names = NEWER ? authenticators.names : authenticators.keys auth_types = auth_names.sort.join ', ' opts.on("-a", "--auth AUTH", auth_types, "IMAP authentication type override", "Authentication type will be auto-", "discovered", "Default: #{options[:Auth].inspect}", "Options file name: :Auth") do |auth| options[:Auth] = auth end opts.separator '' opts.separator "IMAP options:" opts.on("-r", "--root ROOT", "Root of mailbox hierarchy", "Default: #{options[:Root].inspect}", "Options file name: :Root") do |root| options[:Root] = root end opts.on("-b", "--boxes BOXES", Array, "Comma-separated list of mailbox names", "to search", "Default: #{options[:Boxes].inspect}", "Options file name: :Boxes") do |boxes| options[:Boxes] = boxes end opts.on("-v", "--[no-]verbose", "Be verbose", "Default: #{options[:Verbose].inspect}", "Options file name: :Verbose") do |verbose| options[:Verbose] = verbose end opts.on("-n", "--noop", "Perform no destructive operations", "Best used with the verbose option", "Default: #{options[:Noop].inspect}", "Options file name: Noop") do |noop| options[:Noop] = noop end opts.on("-q", "--quiet", "Be quiet") do options[:Verbose] = false end if block_given? then opts.separator '' opts.separator "#{self} options:" yield opts, options if block_given? end @@extra_options.each do |block| block.call opts, options end opts.separator '' opts.banner << <<-EOF Options may also be set in the options file ~/.#{@@opts_file_name} Example ~/.#{@@opts_file_name}: \tHost=mail.example.com \tPassword=my password EOF end # OptionParser.new do op.parse! args.dup options[:Port] ||= options[:SSL] ? 993 : 143 # HACK: removed :Boxes -- push down required_keys = [:Host, :Password] + required_options.keys if required_keys.any? { |k| options[k].nil? } then $stderr.puts op $stderr.puts $stderr.puts "Host name not set" if options[:Host].nil? $stderr.puts "Password not set" if options[:Password].nil? $stderr.puts "Boxes not set" if options[:Boxes].nil? required_options.each do |option_name, (_, missing_message)| $stderr.puts missing_message if options[option_name].nil? end exit 1 end options } # defaults.map end
Handles processing of args
loading defaults from a file in ~ based on processor_file
. Extra option defaults can be specified by required_options
. Yields an option parser instance to add new OptionParser options to:
class MyProcessor < IMAPProcessor def self.process_args(args) required_options = { :MoveTo => [nil, "MoveTo not set"], } super __FILE__, args, required_options do |opts, options| opts.banner << "Explain my_processor's executable" opts.on( "--move=MAILBOX", "Mailbox to move message to", "Default: #{options[:MoveTo].inspect}", "Options file name: :MoveTo") do |mailbox| options[:MoveTo] = mailbox end end end end
NOTE: You can add a –move option using ::add_move
Source
# File lib/imap_processor.rb, line 327 def self.run(args = ARGV, &block) client = nil multi_options = process_args args multi_options.each do |options| client = new(options, &block) client.run end rescue Interrupt exit rescue SystemExit raise rescue Exception => e $stderr.puts "Failed to finish with exception: #{e.class}:#{e.message}" $stderr.puts "\t#{e.backtrace.join "\n\t"}" exit 1 ensure client.imap.logout if client and client.imap end
Sets up an IMAP processor’s options then calls its #run method.
Public Instance Methods
Source
# File lib/imap_processor.rb, line 363 def capability imap, res = nil return imap.capability unless res data = res.data if data.code and data.code.name == 'CAPABILITY' then case data.code.data when Array then data.code.data when String then data.code.data.split ' ' else raise ArgumentError, "unknown type: #{data.code.data.class}" end else imap.capability end end
Extracts capability information for imap
from res
or by contacting the server.
Source
# File lib/imap_processor.rb, line 390 def connect(host = @options[:Host], port = @options[:Port], ssl = @options[:SSL], username = @options[:Username], password = @options[:Password], auth = @options[:Auth]) # :yields: Connection imap = Net::IMAP.new host, port:port, ssl:ssl log "Connected to imap://#{host}:#{port}/" capabilities = capability imap, imap.greeting log "Capabilities: #{capabilities.join ', '}" auth_caps = capabilities.select { |c| c =~ /^AUTH/ } unless auth then raise "Couldn't find a supported auth type" if auth_caps.empty? auth = auth_caps.first.sub(/AUTH=/, '') end # Net::IMAP supports using AUTHENTICATE with LOGIN, PLAIN, and # CRAM-MD5... if the server reports a different AUTH method, then we # should fall back to using LOGIN if %w( LOGIN PLAIN CRAM-MD5 XOAUTH2 ).include?( auth.upcase ) auth = auth.upcase log "Trying #{auth} authentication" res = imap.authenticate auth, username, password log "Logged in as #{username} using AUTHENTICATE" else log "Trying to authenticate via LOGIN" res = imap.login username, password log "Logged in as #{username} using LOGIN" end # CAPABILITY may have changed capabilities = capability imap, res connection = Connection.new imap, capabilities if block_given? then begin yield connection ensure connection.imap.logout end else return connection end end
Connects to IMAP server host
at port
using ssl if ssl
is true then authenticates with username
and password
. IMAPProcessor
is only known to work with PLAIN auth on SSL sockets. IMAPProcessor
does not support LOGIN.
Returns a Connection
object.
Source
# File lib/imap_processor.rb, line 444 def create_mailbox name log "LIST #{name}" list = imap.list '', name return if list && !list.empty? log "CREATE #{name}" imap.create name unless noop? end
Create the mailbox name
if it doesn’t exist. Note that this will SELECT the mailbox if it exists.
Source
# File lib/imap_processor.rb, line 455 def delete_messages uids, expunge = true log "DELETING [...#{uids.size} uids]" imap.store uids, '+FLAGS.SILENT', [:Deleted] unless noop? if expunge then log "EXPUNGE" imap.expunge unless noop? end end
Delete and expunge
the specified uids
.
Source
# File lib/imap_processor.rb, line 476 def each_message(uids, type) # :yields: TMail::Mail parts = mime_parts uids, type uids = [] each_part parts, true do |uid, message| mail = TMail::Mail.parse message begin success = yield uid, mail uids << uid if success rescue => e log e.message puts "\t#{e.backtrace.join "\n\t"}" unless $DEBUG # backtrace at bottom log "Subject: #{mail.subject}" log "Message-Id: #{mail.message_id}" p mail.body if verbose? raise if $DEBUG end end uids end
Yields each uid and message as a TMail::Message for uids
of MIME type type
.
If there’s an exception raised during handling a message the subject, message-id and inspected body are logged.
If the block returns nil or false, the message is considered skipped and its uid is not returned in the uid list. (Hint: next false unless …)
Returns the uids of successfully handled messages.
Source
# File lib/imap_processor.rb, line 506 def each_part(parts, header = false) # :yields: uid, message parts.each do |uid, section| sequence = ["BODY[#{section}]"] sequence.unshift "BODY[#{section}.MIME]" unless section == 'TEXT' sequence.unshift 'BODY[HEADER]' if header body = imap.fetch(uid, sequence).first sequence = sequence.map { |item| body.attr[item] } unless section == 'TEXT' and header then sequence[0].sub!(/\r\n\z/, '') end yield uid, sequence.join end end
Yields each message part from parts
. If header
is true, a complete message is yielded, appropriately joined for use with TMail::Mail.
Source
# File lib/imap_processor.rb, line 527 def log(message) return unless @verbose $stderr.puts "# #{message}" end
Logs message
to $stderr if verbose
Source
# File lib/imap_processor.rb, line 541 def mime_parts(uids, mime_type) media_type, subtype = mime_type.upcase.split('/', 2) structures = imap.fetch uids, 'BODYSTRUCTURE' structures.zip(uids).map do |body, uid| section = nil structure = body.attr['BODYSTRUCTURE'] case structure when Net::IMAP::BodyTypeMultipart then parts = structure.parts section = parts.each_with_index do |part, index| break index if part.media_type == media_type and part.subtype == subtype end next unless Integer === section when Net::IMAP::BodyTypeText, Net::IMAP::BodyTypeBasic then section = 'TEXT' if structure.media_type == media_type and structure.subtype == subtype end [uid, section] end.compact end
Retrieves the BODY data item name for the mime_type
part from messages uids
. Returns an array of uid/part pairs. If no matching part with mime_type
is found the uid is omitted.
Returns an Array of uid, section pairs.
Use a subsequent Net::IMAP#fetch to retrieve the selected part.
Source
# File lib/imap_processor.rb, line 573 def move_messages uids, destination, expunge = true return if uids.empty? verb = expunge ? "MOVE" : "COPY" log "%s %d uids to %s:" % [verb, uids.size, destination] begin imap.copy uids, destination unless noop? rescue Net::IMAP::NoResponseError unless noop? then create_mailbox destination imap.copy uids, destination end end delete_messages uids, expunge end
Move the specified uids
to a new destination
then delete and expunge
them. Creates the destination mailbox if it doesn’t exist.
Source
# File lib/imap_processor.rb, line 617 def noop? options[:Noop] end
Did the user set –noop?
Source
# File lib/imap_processor.rb, line 593 def show_messages(uids) return if uids.nil? or (Array === uids and uids.empty?) fetch_data = +'BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT)]' messages = imap.fetch uids, fetch_data fetch_data.sub! '.PEEK', '' # stripped by server messages ||= [] messages.each do |res| puts res.attr[fetch_data].delete("\r").gsub(/^/, " ") end end
Displays Date, Subject and Message-Id from messages in uids
Source
# File lib/imap_processor.rb, line 610 def verbose? @verbose end
Did the user set –verbose?