Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 265 lines (206 sloc) 8.994 kb
089d258 working script
R. H. Gracini Guiraldelli authored
1 #!/usr/bin/env python
2
3 # BSD LICENSE:
4 # Copyright (c) 2011, Ricardo H Gracini Guiraldelli <rguira@acm.org>
53e7893 @lucasdemarchi Add additional copyrights
lucasdemarchi authored
5 # Copyright (c) 2011, Pedro Pedruzzi <pedro.pedruzzi@gmail.com>
6 # Copyright (c) 2011, Lucas De Marchi <lucas.de.marchi@gmail.com>
089d258 working script
R. H. Gracini Guiraldelli authored
7 # All rights reserved.
1b1067f @lucasdemarchi Remove trailing whitespaces
lucasdemarchi authored
8 #
089d258 working script
R. H. Gracini Guiraldelli authored
9 # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1b1067f @lucasdemarchi Remove trailing whitespaces
lucasdemarchi authored
10 #
089d258 working script
R. H. Gracini Guiraldelli authored
11 # Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12 # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
13 # Neither the name of the Ricardo H Gracini Guiraldelli nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
14 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
16 import imaplib
17 import re
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
18 import rfc822
19 import StringIO
20 import email.header
21 import getpass
22 import os
76356ed @lucasdemarchi import sys
lucasdemarchi authored
23 import sys
6c82bc9 @lucasdemarchi Add option parser support
lucasdemarchi authored
24 from optparse import OptionParser
25
26 USAGE = "%prog [OPTIONS]"
27 VERSION = '0.1'
340f305 @lucasdemarchi Move encoding setup to its own function
lucasdemarchi authored
28 default_encoding = sys.stdout.encoding
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
29 options = None
30 options_defaults = {
31 'host': 'imap.gmail.com',
32 'port': '993',
33 'size': '10'
34 }
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
35
36 # copied from http://docs.python.org/library/imaplib.html
37 list_response_pattern = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
38
39 #imaplib.Debug = 4
40
340f305 @lucasdemarchi Move encoding setup to its own function
lucasdemarchi authored
41 def setup_encoding():
42 global default_encoding
43
44 if not default_encoding or not sys.stdout.isatty():
45 import locale
46 default_encoding = locale.getpreferredencoding()
47
48 if not default_encoding:
49 default_encoding = 'ascii'
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
50
51 def parse_list_response(line):
52 flags, delimiter, mailbox_name = list_response_pattern.match(line).groups()
53 mailbox_name = mailbox_name.strip('"')
54 return (flags, delimiter, mailbox_name)
55
56 # FIXME: not sure if it works always. see: http://bugs.python.org/issue5305
57 def decode_modified_utf7(s):
58 ascii_mode = 1
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
59 r = [ 0 ] * len(s)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
60 for i in range(len(s)):
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
61 r[i] = s[i]
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
62 if ascii_mode:
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
63 if r[i] == '&':
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
64 ascii_mode = 0
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
65 r[i] = '+'
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
66 else:
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
67 if r[i] == ',':
68 r[i] = '/'
69 elif r[i] == '-':
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
70 ascii_mode = 1
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
71 # list -> str
72 r = ''.join(r)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
73 # workaround for http://bugs.python.org/issue4425
d119f47 @pedrox fix decode_modified_utf7 logic. it was broken.
pedrox authored
74 r = r.replace('/', '+AC8-')
75 r = r.decode('utf7')
76 return r
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
77
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
78 def fetch_dump_subject(conn, message_set):
79 if not message_set:
80 return
81
82 status, data = conn.fetch(message_set, '(BODY[HEADER.FIELDS (SUBJECT)])')
83 for piece in data:
84 if isinstance(piece, tuple):
85 dump_subject(piece[1])
86
87 def dump_subject(header):
88 # workaround for http://bugs.python.org/issue504152
89 header = header.replace('\r\n ', ' ')
90 msg = rfc822.Message(StringIO.StringIO(header))
7928130 @dweiss Key error on empty subject (non-existent subject) fix.
authored
91 if (msg.has_key("subject")):
92 sub = msg["subject"]
93 data = email.header.decode_header(sub)
94 sub = data[0][0]
95 subcharset = data[0][1]
96 if subcharset != None:
97 sub = sub.decode(subcharset)
98 safe_print('\tSubject: [%s].' % (sub))
99 else:
100 safe_print('\tSubject: [%s].' % ("[no subject]"))
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
101
e8796fc @pedrox replaces print with safe_print to avoid unmapped unicode characteres exc...
pedrox authored
102 def safe_print(u):
c37658c @lucasdemarchi Guess the encoding to use
lucasdemarchi authored
103 u = u.encode(default_encoding, 'replace')
82acced @lucasdemarchi treat print as a function call
lucasdemarchi authored
104 print(u)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
105
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
106 def input_or_default(prompt, option):
107 global options
108
109 if not (option is None) and not (options.__dict__[option] is None):
110 return options.__dict__[option]
111
112 if option in options_defaults:
113 ret = raw_input("%s [ %s ]: " % (prompt, options_defaults[option]))
114 if len(ret) == 0:
115 ret = options_defaults[option]
116 else:
117 ret = raw_input("%s: " % prompt)
118
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
119 return ret
120
121 def process(host, port, username, password, size, use_ssl=False):
122 # FIXME: make this a parameter
123 dest = 'BIGMAIL'
124
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
125 safe_print("\t Connecting to %s:%d..." % (host, port))
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
126
127 # connect to IMAP server
128 if use_ssl:
129 imap_connection = imaplib.IMAP4_SSL(host, port)
130 else:
131 imap_connection = imaplib.IMAP4(host, port)
132
133 # authenticate by plain-text login
134 imap_connection.login(username, password)
135
136 # list and print mailboxes
137 status, boxes = imap_connection.list()
138
139 box = 1
140
141 # FIXME: this function should not be interactive
142 for ibox in range(len(boxes)):
143 # TODO: filter \Noselect flagged mailboxes
144 boxes[ibox] = parse_list_response(boxes[ibox])[2]
145
146 decoded = decode_modified_utf7(boxes[ibox])
147 if decoded == '[Gmail]/All Mail':
148 box = ibox + 1
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
149 safe_print("%d. %s" % (ibox + 1, decoded))
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
150
151 # prompt for a mailbox
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
152 box = boxes[int(input_or_default("Mailbox", None)) - 1]
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
153
154 # select mailbox
155 status, data = imap_connection.select(box)
156 if status == 'NO':
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
157 safe_print(data)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
158
159 # print mailbox status
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
160 safe_print("\tYou have %s messages in mailbox '%s'." % (data[0], decode_modified_utf7(box)))
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
161
162 remsgsize = re.compile("(\d+) \(RFC822.SIZE (\d+).*\)")
163
164 msg_set = StringIO.StringIO()
165
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
166 safe_print("\tLooking up big e-mails...")
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
167
168 status, data = imap_connection.fetch('1:*', '(RFC822.SIZE)')
169 count = 0
170 for msg in data:
171 match = remsgsize.match(msg)
172 msgid = int(match.group(1))
173 msgsize = int(match.group(2))
174
175 if msgsize >= size:
bf6ca2d @lucasdemarchi function calls need parentheses
lucasdemarchi authored
176 #safe_print("to move: id=" + str(msgid) + ", size=" + str(msgsize))
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
177 msg_set.write(str(msgid))
178 msg_set.write(",")
179 count = count + 1
180
181 # remove trailing comma
182 msg_set.seek(-1, os.SEEK_CUR)
183 msg_set.truncate()
184
185 # StringIO -> str
186 msg_set = msg_set.getvalue()
187
5269837 @lucasdemarchi Fix case when 0 emails were found
lucasdemarchi authored
188 safe_print("\tDone. %d e-mails found." % count)
85264b8 @lucasdemarchi Rename non-test functions
lucasdemarchi authored
189 fetch_dump_subject(imap_connection, msg_set)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
190
5269837 @lucasdemarchi Fix case when 0 emails were found
lucasdemarchi authored
191 if count == 0:
192 safe_print("\tNothing to do. Closing connection")
193 else:
194 safe_print("\tCopying emails to mailbox '%s'..." % dest)
195
196 # create destination mailbox, if new
197 status, data = imap_connection.create(dest)
198 if status == 'NO':
199 pass
200 # we can ignore this failure assuming it is about a preexisting mailbox
201 # if it is not the case, than the copy will fail next
202
203 # copy to destination mailbox
204 status, data = imap_connection.copy(msg_set, dest)
205 if status == 'NO':
206 safe_print(data)
207
208 # TODO: remove e-mails from original mailbox when it makes sense
209 # users generally want to _move_ big e-mails to separate mailboxes.
210 # however, some mail servers (like google's for instance) have a label/tag
211 # semantics for mailboxes thus making no point in removing a big e-mail
212 # from such a mailbox.
213 safe_print("\tDone! Closing connection")
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
214
215 # close and sync selected mailbox
216 imap_connection.close()
217
218 # logout and close connection
219 imap_connection.logout()
220 imap_connection.shutdown()
221
222
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
223 def parse_options(args):
224 parser = OptionParser(usage=USAGE, version=VERSION)
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
225 parser.add_option('-H', '--host', type='string',
226 help='IMAP server hostname')
227 parser.add_option('-p', '--port', type='string',
228 help='IMAP server port')
229 parser.add_option('-u', '-U', '--user', type='string',
230 help='IMAP username')
231 parser.add_option('-s', '--size', type='string',
232 help='Min email size to search for')
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
233
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
234 return parser.parse_args()
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
235
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
236 def main(*args):
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
237 global options
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
238
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
239 options, args = parse_options(args)
240 setup_encoding()
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
241
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
242 host = input_or_default('IMAP server hostname', 'host')
243 port = int(input_or_default('IMAP server port', 'port'))
244 size = int(input_or_default('Minimum size in MB', 'size'))
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
245
a826d91 @lucasdemarchi Add command line options
lucasdemarchi authored
246 username = input_or_default('Login', 'user')
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
247 password = getpass.getpass('Password: ')
248
249 # convert to bytes
250 size = size * 1024 * 1024
251
252 process(host, port, username, password, size, True)
4fe76c7 @pedrox a lot of improvements. to summarize:
pedrox authored
253
254
6c82bc9 @lucasdemarchi Add option parser support
lucasdemarchi authored
255 if __name__ == '__main__':
256 sys.exit(main(*sys.argv))
ab8c3d4 @lucasdemarchi Re-organize function order
lucasdemarchi authored
257
258
259 ########################## TESTS
260
261 def test():
262 process('imap.gmail.com', 993, 'any@gmail.com', 'thing', 10 * 1024 * 1024, True)
263
264 #test()
Something went wrong with that request. Please try again.