some fixups to snarf_mail scripts
[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 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 or cmccabe@company.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.  Example: imap.gmail.com") 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 format_date(date)
94   date.gsub!(' ', '_')
95 end
96
97 def get_sanitized_email_name(mailbox, arr)
98   msn = mailbox.dup
99   msn.gsub!(' ', '_')
100   msn.gsub!('/', '.')
101   return "#{msn}_#{format_date(arr["INTERNALDATE"])}_#{format_uid(arr["UID"])}"
102 end
103
104 def write_email_to_disk(mailbox, data)
105   arr = data[0].attr
106   filename = get_sanitized_email_name(mailbox, arr)
107   fp = File.open(filename, 'w')
108   fp.write(arr["RFC822.HEADER"])
109   fp.write(arr["RFC822.TEXT"])
110   fp.close
111 end
112
113 def snarf_mailbox(imap, mailbox)
114   full_count = 0
115   first_time = true
116
117   searchterms = [ "NOT", "DELETED" ]
118   if $opts.delete == "old"
119     t = Date.today() - 365
120     time_str = t.strftime("%e-%b-%Y")
121     searchterms << "BEFORE" << time_str
122     prequel = "fetched and deleted: "
123   elsif $opts.delete == "none"
124     prequel = "fetched: "
125   else
126     raise "expected one of 'old', 'none' for delete argument."
127   end
128
129   while true
130     count = 0
131     msg_seqnos = Array.new
132
133     imap.select(mailbox)
134     imap.search(searchterms).each do |message_id|
135       if (first_time == true) then
136         # Print a dot immediately after making first contact with the server.
137         # It is reassuring to the user.
138         printf(".")
139         STDOUT.flush()
140         first_time = false
141       end
142       data = imap.fetch(message_id, 
143                 [ "INTERNALDATE", "UID", "RFC822.HEADER", "RFC822.TEXT" ])
144       write_email_to_disk(mailbox, data)
145       count = count + 1
146       full_count = full_count + 1
147       msg_seqnos << data[0].seqno.to_i
148       #break if (count > 20)
149     end
150     if (count == 0) then
151       puts "#{prequel} #{full_count} messages from #{mailbox}"
152       return
153     end
154
155     # Print out a dot to signify progress
156     printf(".")
157     STDOUT.flush()
158
159     if $opts.delete != "none" then
160       # Delete messages
161       imap.store(msg_seqnos, "+FLAGS", [:Deleted])
162       imap.expunge
163     end
164   end
165 end
166
167 # MAIN
168 begin
169   $opts = MyOptions.parse(ARGV)
170 rescue Exception => msg
171   $stderr.print("#{msg}.\nType --help to see usage information.\n")
172   exit 1
173 end
174
175 password = get_password("Please enter the password for #{$opts.username}:")
176 imap = Net::IMAP.new($opts.server, 993, true)
177 imap.login($opts.username, password)
178 case ($opts.action)
179 when :list
180   imap.list("", "*").each do |mbl|
181     puts "#{mbl.name}"
182   end
183 when :snarf
184   $opts.mailboxes.each do |mailbox|
185     snarf_mailbox(imap, mailbox)
186   end
187 else
188   raise "unknown action #{$opts.action}"
189 end
190 imap.logout()
191 imap.disconnect()
192 exit 0