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