ccc: handle .rb files
[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 snarf_mailbox(imap, mailbox)
93   full_count = 0
94   first_time = true
95   while true
96     count = 0
97     msg_seqnos = Array.new
98
99     imap.select(mailbox)
100     imap.search(["NOT", "DELETED"]).each do |message_id|
101       if (first_time == true) then
102         # Print a dot immediately after making first contact with the server.
103         # It is reassuring to the user.
104         printf(".")
105         STDOUT.flush()
106         first_time = false
107       end
108       data = imap.fetch(message_id, [ "UID", "RFC822.HEADER", "RFC822.TEXT" ])
109       arr = data[0].attr
110       filename = "#{mailbox}#{format_uid(arr["UID"])}"
111       fp = File.open(filename, 'w')
112       fp.write(arr["RFC822.HEADER"])
113       fp.write(arr["RFC822.TEXT"])
114       fp.close
115       count = count + 1
116       full_count = full_count + 1
117       msg_seqnos << data[0].seqno.to_i
118       break if (count > 20)
119     end
120     if (count == 0) then
121       action_str = ($opts.delete_after == true) ?
122         "fetched and deleted" : "fetched"
123       puts "#{action_str} #{full_count} messages from #{mailbox}"
124       return
125     end
126
127     # Print out a dot to signify progress
128     printf(".")
129     STDOUT.flush()
130
131     # Delete messages if we're supposed to
132     if ($opts.delete_after == true) then
133       imap.store(msg_seqnos, "+FLAGS", [:Deleted])
134       imap.expunge
135     end
136   end
137 end
138
139 # MAIN
140 begin
141   $opts = MyOptions.parse(ARGV)
142 rescue Exception => msg
143   $stderr.print("#{msg}.\nType --help to see usage information.\n")
144   exit 1
145 end
146
147 password = get_password("Please enter the password for #{$opts.username}:")
148 imap = Net::IMAP.new($opts.server, 993, true)
149 imap.login($opts.username, password)
150 case ($opts.action)
151 when :list
152   imap.list("", "*").each do |mbl|
153     puts "#{mbl.name}"
154   end
155 when :snarf
156   $opts.mailboxes.each do |mailbox|
157     snarf_mailbox(imap, mailbox)
158   end
159 else
160   raise "unknown action #{$opts.action}"
161 end
162 imap.logout()
163 imap.disconnect()
164 exit 0