6 # Copies mail from an IMAP account
9 # http://ruby-doc.org/stdlib/libdoc/net/imap/rdoc/index.html
17 # Check that we were passed a date in %e-%b-%Y format.
20 reserialized = d.strftime("%e-%b-%Y")
22 if (reserialized != input) then
23 raise "Invalid date '" + input + "': reserialized form was " + \
24 "'" + reserialized + "', which does not match original."
31 opts.mailboxes = Array.new
35 # Fill in $opts values
36 parser = OptionParser.new do |myparser|
37 myparser.banner = "Usage: #{ File.basename($0) } [opts]"
38 myparser.separator("Specific options:")
39 myparser.on("--since [TIME]", "-1",
40 "Time to fetch email messages since. (example: \
43 check_date(opts.since)
45 myparser.on("--before [TIME]", "-2",
46 "Time to fetch email messages before. (example: \
49 check_date(opts.before)
51 myparser.on("--username [USERNAME]", "-u",
52 "Email account to fetch. (example: \
53 RareCactus@gmail.com or cmccabe@company.com)") do |u|
56 myparser.on("--list-folders", "-l",
57 "List the IMAP folders that are present.") do |a|
58 raise "can only specify one action" if (opts.action)
61 myparser.on("--snarf", "-S",
62 "Copy mail to the current directory.") do |a|
63 raise "can only specify one action" if (opts.action)
66 myparser.on("--box [MAILBOX]", "-b",
67 "Act on a given mailbox. You may specify -b more than once for \
68 multiple mailboxes.") do |a|
71 myparser.on("--server [SERVER]", "-s",
72 "Email server to use. Example: imap.gmail.com") do |u|
78 raise "must specify an action" unless opts.action
79 raise "must give a username" unless opts.username
80 raise "must give a server" unless opts.server
81 if ((opts.action == :snarf) and opts.mailboxes.empty?()) then
82 raise "you must specify mailbox(es) with -b in order to snarf"
88 # Get a password from STDIN without echoing it.
89 # This is kind of ugly, but it does work.
90 def get_password(prompt)
91 shell_cmds = 'stty -echo && read password && echo ${password}'
95 pipe = IO.popen(shell_cmds, "r") do |pipe|
98 echo_status = $?.exitstatus
101 if (echo_status != 0) then
102 raise "get_password: error executing: #{shell_cmds}"
108 # We don't know how to deal with non-numeric UIDs. Best just to leave them
110 return uid if (uid =~ /[^0123456789]/)
112 # Pad numeric uids out to 6 digits
113 return sprintf("%006d", uid)
116 def format_date(date)
120 def get_sanitized_email_name(mailbox, arr)
124 return "#{msn}_#{format_date(arr["INTERNALDATE"])}_#{format_uid(arr["UID"])}"
127 def write_email_to_disk(mailbox, data)
129 filename = get_sanitized_email_name(mailbox, arr)
130 fp = File.open(filename, 'w')
131 fp.write(arr["RFC822.HEADER"])
132 fp.write(arr["RFC822.TEXT"])
136 def snarf_mailbox(imap, mailbox)
140 searchterms = [ "NOT", "DELETED" ]
141 if $opts.since != nil then
142 searchterms << "SINCE" << $opts.since
144 if $opts.before != nil then
145 searchterms << "BEFORE" << $opts.before
147 print "using IMAP search terms: " + searchterms.join(" ")
148 prequel = "fetched: "
152 msg_seqnos = Array.new
155 imap.search(searchterms).each do |message_id|
156 if (first_time == true) then
157 # Print a dot immediately after making first contact with the server.
158 # It is reassuring to the user.
163 data = imap.fetch(message_id,
164 [ "INTERNALDATE", "UID", "RFC822.HEADER", "RFC822.TEXT" ])
165 write_email_to_disk(mailbox, data)
167 full_count = full_count + 1
168 msg_seqnos << data[0].seqno.to_i
169 #break if (count > 20)
172 puts "#{prequel} #{full_count} messages from #{mailbox}"
176 # Print out a dot to signify progress
180 if $opts.delete != "none" then
182 imap.store(msg_seqnos, "+FLAGS", [:Deleted])
190 $opts = MyOptions.parse(ARGV)
191 rescue Exception => msg
192 $stderr.print("#{msg}.\nType --help to see usage information.\n")
196 password = get_password("Please enter the password for #{$opts.username}:")
197 imap = Net::IMAP.new($opts.server, 993, true)
198 imap.login($opts.username, password)
201 imap.list("", "*").each do |mbl|
205 $opts.mailboxes.each do |mailbox|
206 snarf_mailbox(imap, mailbox)
209 raise "unknown action #{$opts.action}"