Makefile: add pickrand
[cmccabe-bin] / snarf_mail_imap.rb
1 #!/usr/bin/env ruby
2
3 #
4 # snarf_mail.rb
5 #
6 # Copies mail from an IMAP account
7 #
8 # Handy reference:
9 # http://ruby-doc.org/stdlib/libdoc/net/imap/rdoc/index.html
10 #
11
12 require 'date'
13 require 'net/imap'
14 require 'optparse'
15 require 'ostruct'
16
17 # Check that we were passed a date in %e-%b-%Y format.
18 def check_date(input)
19   d = Date.parse(input)
20   reserialized = d.strftime("%e-%b-%Y")
21   reserialized.strip!
22   if (reserialized != input) then
23     raise "Invalid date '" + input + "': reserialized form was " + \
24       "'" + reserialized + "', which does not match original."
25   end
26 end
27
28 class MyOptions
29   def self.parse(args)
30     opts = OpenStruct.new
31     opts.mailboxes = Array.new
32     opts.since = nil
33     opts.before = nil
34
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: \
41  1-Dec-2010.)") do |u|
42         opts.since = u
43         check_date(opts.since)
44       end
45       myparser.on("--before [TIME]", "-2",
46               "Time to fetch email messages before.  (example: \
47  1-Dec-2011.)") do |u|
48         opts.before = u
49         check_date(opts.before)
50       end
51       myparser.on("--username [USERNAME]", "-u",
52               "Email account to fetch. (example: \
53 RareCactus@gmail.com or cmccabe@company.com)") do |u|
54         opts.username = u
55       end
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)
59         opts.action = :list
60       end
61       myparser.on("--snarf", "-S",
62               "Copy mail to the current directory.") do |a|
63         raise "can only specify one action" if (opts.action)
64         opts.action = :snarf
65       end
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|
69         opts.mailboxes << a
70       end
71       myparser.on("--server [SERVER]", "-s",
72               "Email server to use.  Example: imap.gmail.com") do |u|
73         opts.server = u
74       end
75     end
76
77     parser.parse!(args)
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"
83     end
84     return opts
85   end
86 end
87
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}'
92   printf "#{prompt}"
93   STDOUT.flush
94   pass = ""
95   pipe = IO.popen(shell_cmds, "r") do |pipe|
96     pass = pipe.read
97   end
98   echo_status = $?.exitstatus
99   system("stty sane")
100   puts
101   if (echo_status != 0) then
102     raise "get_password: error executing: #{shell_cmds}"
103   end
104   return pass.chomp
105 end
106
107 def format_uid(uid)
108   # We don't know how to deal with non-numeric UIDs. Best just to leave them
109   # alone.
110   return uid if (uid =~ /[^0123456789]/)
111
112   # Pad numeric uids out to 6 digits
113   return sprintf("%006d", uid)
114 end
115
116 def format_date(date)
117   date.gsub!(' ', '_')
118 end
119
120 def get_sanitized_email_name(mailbox, arr)
121   msn = mailbox.dup
122   msn.gsub!(' ', '_')
123   msn.gsub!('/', '.')
124   return "#{msn}_#{format_date(arr["INTERNALDATE"])}_#{format_uid(arr["UID"])}"
125 end
126
127 def write_email_to_disk(mailbox, data)
128   arr = data[0].attr
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"])
133   fp.close
134 end
135
136 def snarf_mailbox(imap, mailbox)
137   full_count = 0
138   first_time = true
139
140   searchterms = [ "NOT", "DELETED" ]
141   if $opts.since != nil then
142     searchterms << "SINCE" << $opts.since
143   end
144   if $opts.before != nil then
145     searchterms << "BEFORE" << $opts.before
146   end
147   print "using IMAP search terms: " + searchterms.join(" ")
148   prequel = "fetched: "
149
150   while true
151     count = 0
152     msg_seqnos = Array.new
153
154     imap.select(mailbox)
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.
159         printf(".")
160         STDOUT.flush()
161         first_time = false
162       end
163       data = imap.fetch(message_id, 
164                 [ "INTERNALDATE", "UID", "RFC822.HEADER", "RFC822.TEXT" ])
165       write_email_to_disk(mailbox, data)
166       count = count + 1
167       full_count = full_count + 1
168       msg_seqnos << data[0].seqno.to_i
169       #break if (count > 20)
170     end
171     if (count == 0) then
172       puts "#{prequel} #{full_count} messages from #{mailbox}"
173       return
174     end
175
176     # Print out a dot to signify progress
177     printf(".")
178     STDOUT.flush()
179
180     if $opts.delete != "none" then
181       # Delete messages
182       imap.store(msg_seqnos, "+FLAGS", [:Deleted])
183       imap.expunge
184     end
185   end
186 end
187
188 # MAIN
189 begin
190   $opts = MyOptions.parse(ARGV)
191 rescue Exception => msg
192   $stderr.print("#{msg}.\nType --help to see usage information.\n")
193   exit 1
194 end
195
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)
199 case ($opts.action)
200 when :list
201   imap.list("", "*").each do |mbl|
202     puts "#{mbl.name}"
203   end
204 when :snarf
205   $opts.mailboxes.each do |mailbox|
206     snarf_mailbox(imap, mailbox)
207   end
208 else
209   raise "unknown action #{$opts.action}"
210 end
211 imap.logout()
212 imap.disconnect()
213 exit 0