fc51b57d5024c488687bcb7c14c7edf40a6906f8
[cmccabe-bin] / snarf_mail.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 class MyOptions
18   def self.parse(args)
19     opts = OpenStruct.new
20     opts.mailboxes = Array.new
21     opts.delete = "none"
22
23     # Fill in $opts values
24     parser = OptionParser.new do |myparser|
25       myparser.banner = "Usage: #{ File.basename($0) } [opts]"
26       myparser.separator("Specific options:")
27       myparser.on("--delete POLICY", "-d",
28               "Set delete policy to 'none' or 'old'. Default is 'none'.") do |d|
29         opts.delete = d
30       end
31       myparser.on("--username USERNAME", "-u",
32               "Email account to fetch. (example: \
33 RareCactus@gmail.com)") do |u|
34         opts.username = u
35       end
36       myparser.on("--list-folders", "-l",
37               "List the IMAP folders that are present.") do |a|
38         raise "can only specify one action" if (opts.action)
39         opts.action = :list
40       end
41       myparser.on("--snarf", "-S",
42               "Copy mail to the current directory.") do |a|
43         raise "can only specify one action" if (opts.action)
44         opts.action = :snarf
45       end
46       myparser.on("--box [MAILBOX]", "-b",
47               "Act on a given mailbox. You may specify -b more than once for \
48 multiple mailboxes.") do |a|
49         opts.mailboxes << a
50       end
51       myparser.on("--server [SERVER]", "-s",
52               "Email server to use") do |u|
53         opts.server = u
54       end
55     end
56
57     parser.parse!(args)
58     raise "must specify an action" unless opts.action
59     raise "must give a username" unless opts.username
60     raise "must give a server" unless opts.server
61     return opts
62   end
63 end
64
65 # Get a password from STDIN without echoing it.
66 # This is kind of ugly, but it does work.
67 def get_password(prompt)
68   shell_cmds = 'stty -echo && read password && echo ${password}'
69   printf "#{prompt}"
70   STDOUT.flush
71   pass = ""
72   pipe = IO.popen(shell_cmds, "r") do |pipe|
73     pass = pipe.read
74   end
75   echo_status = $?.exitstatus
76   system("stty sane")
77   puts
78   if (echo_status != 0) then
79     raise "get_password: error executing: #{shell_cmds}"
80   end
81   return pass.chomp
82 end
83
84 def format_uid(uid)
85   # We don't know how to deal with non-numeric UIDs. Best just to leave them
86   # alone.
87   return uid if (uid =~ /[^0123456789]/)
88
89   # Pad numeric uids out to 6 digits
90   return sprintf("%006d", uid)
91 end
92
93 def get_sanitized_email_name(mailbox, uid)
94   msn = mailbox.dup
95   msn.gsub!(' ', '_')
96   msn.gsub!('/', '.')
97   return "#{msn}#{format_uid(uid)}"
98 end
99
100 def write_email_to_disk(mailbox, data)
101   arr = data[0].attr
102   filename = get_sanitized_email_name(mailbox, arr["UID"])
103   fp = File.open(filename, 'w')
104   fp.write(arr["RFC822.HEADER"])
105   fp.write(arr["RFC822.TEXT"])
106   fp.close
107 end
108
109 def snarf_mailbox(imap, mailbox)
110   full_count = 0
111   first_time = true
112
113   searchterms = [ "NOT", "DELETED" ]
114   if $opts.delete == "old"
115     t = Date.today() - 365
116     time_str = t.strftime("%e-%b-%Y")
117     searchterms << "BEFORE" << time_str
118     prequel = "fetched and deleted: "
119   elsif $opts.delete == "none"
120     prequel = "fetched: "
121   else
122     raise "expected one of 'old', 'none' for delete argument."
123   end
124
125   while true
126     count = 0
127     msg_seqnos = Array.new
128
129     imap.select(mailbox)
130     imap.search(searchterms).each do |message_id|
131       if (first_time == true) then
132         # Print a dot immediately after making first contact with the server.
133         # It is reassuring to the user.
134         printf(".")
135         STDOUT.flush()
136         first_time = false
137       end
138       data = imap.fetch(message_id, [ "UID", "RFC822.HEADER", "RFC822.TEXT" ])
139       write_email_to_disk(mailbox, data)
140       count = count + 1
141       full_count = full_count + 1
142       msg_seqnos << data[0].seqno.to_i
143       #break if (count > 20)
144     end
145     if (count == 0) then
146       puts "#{prequel} #{full_count} messages from #{mailbox}"
147       return
148     end
149
150     # Print out a dot to signify progress
151     printf(".")
152     STDOUT.flush()
153
154     if ($opts.delete != "none"):
155       # Delete messages
156       imap.store(msg_seqnos, "+FLAGS", [:Deleted])
157       imap.expunge
158     end
159   end
160 end
161
162 # MAIN
163 begin
164   $opts = MyOptions.parse(ARGV)
165 rescue Exception => msg
166   $stderr.print("#{msg}.\nType --help to see usage information.\n")
167   exit 1
168 end
169
170 password = get_password("Please enter the password for #{$opts.username}:")
171 imap = Net::IMAP.new($opts.server, 993, true)
172 imap.login($opts.username, password)
173 case ($opts.action)
174 when :list
175   imap.list("", "*").each do |mbl|
176     puts "#{mbl.name}"
177   end
178 when :snarf
179   $opts.mailboxes.each do |mailbox|
180     snarf_mailbox(imap, mailbox)
181   end
182 else
183   raise "unknown action #{$opts.action}"
184 end
185 imap.logout()
186 imap.disconnect()
187 exit 0