1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """ This module provides the ImapMailbox class, which is a wrapper around
22 the imaplib module of the standard library, and a full implementation
23 of the mailbox.Mailbox interface.
24 """
25
26 import imaplib
27 from email.generator import Generator
28 from cStringIO import StringIO
29 from mailbox import Mailbox
30 from mailbox import Message
31
32 from ProcImap.ImapServer import ImapServer
33 from ProcImap.ImapMessage import ImapMessage
34
35
36 FIX_BUGGY_IMAP_FROMLINE = False
37
38
39
40
41
42
43
45 """ Raised if the imap server returns a non-OK status on any request """
46 pass
47
49 """ Raised if a message is requested with a non-existing UID """
50 pass
51
53 """ Raised if you try to make a change to a mailbox that was opened
54 read-only """
55 pass
56
58 """ Raised if a method is called that the Mailbox interface demands,
59 but that cannot be surported in IMAP
60 """
61 pass
62
64 """ Raised if you try to open a ImapMailbox using an instance of ImapServer
65 that is already used for another ImapMailbox """
66 pass
67
68
69
71 """ An abstract representation of a mailbox on an IMAP Server.
72 This class implements the mailbox.Mailbox interface, insofar
73 possible. Methods for changing a message in-place are not
74 available for IMAP; the 'update' and '__setitem__' methods
75 will raise a NotSupportedError.
76 By default deleting messages (discard/remove) just adds the
77 \\Deleted flag to the message. Optionally, you can define a
78 trash folder for any ImapMailbox. If set, "deleted" messages
79 will be moved to the trash folder. Note that you must set the
80 trash folder to '[Gmail]/Trash' if you are using Gmail, as
81 the Gmail IMAP server has a different interpretation of what
82 deletion means.
83
84 The class specific attributes are:
85
86 name name of the mailbox (readonly, see below)
87 server ImapServer object (readonly, see below)
88 trash Trash folder
89 readonly True if mailbox is readonly, false otherwise
90
91 The 'trash' attribute may a string, another instance
92 of ImapMailbox, or an instance of mailbox.Mailbox.
93 If not set, it is None.
94
95 If the 'readonly' attribute is set, all subsequent calls that would
96 change the mailbox will raise a ReadOnlyError. Note that setting the
97 readonly attribute does not prevent you from making changes through
98 the methods of the server attribute.
99 """
101 """ Initialize an ImapMailbox
102 path is a tuple with two elements, consisting of
103 1) an instance of ImapServer in any state
104 2) the name of a mailbox on the server as a string
105 If the mailbox does not exist, it is created unless
106 create is set to False, in which case NoSuchMailboxError
107 is raised.
108 The 'factory' parameter determines to which type the
109 messages in the mailbox should be converted.
110
111 Note that two instances of ImapMailbox can never share the
112 same instance of server. If you try to create an ImapMailbox
113 with an instance of ImapServer that you already used for
114 another mailbox, a ServerNotAvailableError will be thrown.
115 """
116
117
118 self._factory = factory
119 try:
120 (server, name) = path
121 except:
122 raise TypeError, "path must be a tuple, consisting of an "\
123 + " instance of ImapServer and a string"
124 if isinstance(server, ImapServer):
125 if hasattr(server, 'locked') and server.locked:
126 raise ServerNotAvailableError, "This instance of ImapServer"\
127 + " is already in use for another mailbox"
128 self._server = server
129 else:
130 raise TypeError, "path must be a tuple, consisting of an "\
131 + " instance of ImapServer and a string"
132 if not isinstance(name, str):
133 raise TypeError("path must be a tuple, consisting of an "\
134 + " instance of ImapServer and a string")
135 self._server.select(name, create)
136 self._cached_uid = None
137 self._cached_mailbox = None
138 self._cached_text = None
139 self.trash = None
140 self.readonly = readonly
141 server.locked = True
142
143 name = property(lambda self: self._server.mailboxname, None,
144 doc="Name of the mailbox on the server")
145
146 server = property(lambda self: self._server, None,
147 doc="Instance of the ImapServer that is being used as a backend")
148
163
164
165 - def switch(self, name, readonly=False, create=False):
166 """ Switch to a different Mailbox on the same server """
167 self.flush()
168 if not isinstance(name, str):
169 raise TypeError("name must be the name of a mailbox " \
170 + "as a string")
171 self._server.select(name, create)
172 self._cached_uid = None
173 self._cached_text = None
174 self.readonly = readonly
175
176 - def search(self, criteria='ALL', charset=None ):
177 """ Return a list of all the UIDs in the mailbox (as integers)
178 that match the search criteria. See documentation
179 of imaplib and/or RFC3501 for details.
180 Raise ImapNotOkError if a non-OK response is received from
181 the server or if the response cannot be parsed into a list
182 of integers.
183
184 charset indicates the charset
185 of the strings that appear in the search criteria.
186
187 In all search keys that use strings, a message matches the key if
188 the string is a substring of the field. The matching is
189 case-insensitive.
190
191 The defined search keys are as follows. Refer to RFC 3501 for detailed definitions of the
192 arguments.
193
194 <sequence set>
195 ALL
196 ANSWERED
197 BCC <string>
198 BEFORE <date>
199 BODY <string>
200 CC <string>
201 DELETED
202 DRAFT
203 FLAGGED
204 FROM <string>
205 HEADER <field-name> <string>
206 KEYWORD <flag>
207 LARGER <n>
208 NEW
209 NOT <search-key>
210 OLD
211 ON <date>
212 OR <search-key1> <search-key2>
213 RECENT
214 SEEN
215 SENTBEFORE <date>
216 SENTON <date>
217 SENTSINCE <date>
218 SINCE <date>
219 SMALLER <n>
220 SUBJECT <string>
221 TEXT <string>
222 TO <string>
223 UID <sequence set>
224 UNANSWERED
225 UNDELETED
226 UNDRAFT
227 UNFLAGGED
228 UNKEYWORD <flag>
229 UNSEEN
230
231 Example: search('FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"')
232 search('TEXT "string not in mailbox"')
233 """
234 (code, data) = self._server.uid('search', charset, "(%s)" % criteria)
235 uidlist = data[0].split()
236 if code != 'OK':
237 raise ImapNotOkError, "%s in search" % code
238 try:
239 return [int(uid) for uid in uidlist]
240 except ValueError:
241 raise ImapNotOkError, "received unparsable response."
242
244 """ Get a list of all the unseen UIDs in the mailbox
245 Equivalent to search(None, "UNSEEN UNDELETED")
246 """
247 return(self.search("UNSEEN UNDELETED"))
248
250 """ Get a list of all the undeleted UIDs in the mailbox
251 (as integers).
252 Equivalent to search(None, "UNDELETED")
253 """
254 return(self.search("UNDELETED"))
255
257 """ Download the RFC822 text of the message with UID and put
258 in in the cache. Return the RFC822 text of the message. If the
259 message is already in the cache, it is returned directly.
260 Raise KeyError if there if there is no message with that UID.
261 """
262 if (self._cached_uid != uid) or (self._cached_mailbox != self.name):
263 try:
264 (code, data) = self._server.uid('fetch', uid, "(RFC822)")
265 if code != 'OK':
266 raise ImapNotOkError, "%s in fetch_message(%s)" \
267 % (code, uid)
268 try:
269 rfc822string = data[0][1]
270 except TypeError:
271 raise KeyError, "No message %s in _cache_message" % uid
272 except MemoryError:
273
274
275 self.reconnect()
276 size = self.get_size(uid)
277 octets_read = 0
278 chunksize = 204800
279 chunks = []
280 while octets_read < size:
281 attempts = 0
282 while True:
283 try:
284 (code, data) = self._server.uid('fetch', uid,
285 "(BODY[]<%s.%s>)" % (octets_read, chunksize))
286 if code != 'OK':
287 raise ImapNotOkError, "%s in fetch_message(%s)"\
288 % (code, uid)
289 break
290 except:
291 self.reconnect()
292 attempts += 1
293 continue
294 if attempts > 10:
295 break
296 chunksize = chunksize / (attempts + 1)
297 try:
298 chunks.append(data[0][1])
299 except TypeError:
300 raise KeyError, "No message %s in _cache_message" % uid
301 octets_read += chunksize
302 rfc822string = ''.join(chunks)
303 if FIX_BUGGY_IMAP_FROMLINE:
304 if rfc822string.startswith(">From "):
305 rfc822string = rfc822string[rfc822string.find("\n")+1:]
306 self._cached_uid = uid
307 self._cached_mailbox = self.name
308 self._cached_text = rfc822string
309 return self._cached_text
310
323
325 """ Return an ImapMessage object created from the message with UID.
326 Raise KeyError if there if there is no message with that UID.
327 """
328 return self.get_message(uid)
329
330 - def get(self, uid, default=None):
331 """ Return an ImapMessage object created from the message with UID.
332 Return default if there is no message with that UID.
333 """
334 try:
335 return self[uid]
336 except KeyError:
337 return default
338
340 """ Return a RFC822 string representation of the message
341 corresponding to key, or raise a KeyError exception if no
342 such message exists.
343 """
344 return self._cache_message(uid)
345
347 """ Return a cStringIO.StringIO of the message corresponding
348 to key, or raise a KeyError exception if no such message
349 exists.
350 """
351 return StringIO(self._cache_message(uid))
352
354 """ Return True if key corresponds to a message, False otherwise.
355 """
356 return (uid in self.search('ALL'))
357
359 """ Return True if key corresponds to a message, False otherwise.
360 """
361 return self.has_key(uid)
362
364 """ Return a count of messages in the mailbox. """
365 return len(self.search('ALL'))
366
374
375 - def pop(self, uid, default=None):
376 """ Return a representation of the message corresponding to key,
377 delete and expunge the message. If no such message exists,
378 return default if it was supplied (i.e. is not None) or else
379 raise a KeyError exception. The message is represented as an
380 instance of ImapMessage unless a custom message factory was
381 specified when the Mailbox instance was initialized.
382 """
383 if self.readonly:
384 raise ReadOnlyError, "Tried to pop read-only mailbox"
385 try:
386 message = self[uid]
387 del self[uid]
388 self.expunge()
389 return message
390 except KeyError:
391 if default is not None:
392 return default
393 else:
394 raise KeyError, "No such UID"
395
397 """ Return an arbitrary (key, message) pair, where key is a key
398 and message is a message representation, delete and expunge
399 the corresponding message. If the mailbox is empty, raise a
400 KeyError exception. The message is represented as an instance
401 of ImapMessage unless a custom message factory was specified
402 when the Mailbox instance was initialized.
403 """
404 if self.readonly:
405 raise ReadOnlyError, "Tried to pop item from read-only mailbox"
406 self.expunge()
407 uids = self.search("ALL")
408 if len(uids) > 0:
409 uid = uids[0]
410 result = (uid, self[uid])
411 del self[uid]
412 self.expunge()
413 return result
414 else:
415 raise KeyError, "Mailbox is empty"
416
418 """ Parameter arg should be a key-to-message mapping or an iterable
419 of (key, message) pairs. Updates the mailbox so that, for each
420 given key and message, the message corresponding to key is set
421 to message as if by using __setitem__().
422 This operation is not supported for IMAP mailboxes and will
423 raise NotSupportedError
424 """
425 raise NotSupportedError, "Updating items in IMAP not supported"
426
427
429 """ Equivalent to expunge() """
430 if not self.readonly:
431 self.expunge()
432
434 """ Do nothing """
435 pass
436
438 """ Do nothing """
439 pass
440
442 """ Return an ImapMessage object containing only the Header
443 of the message with UID.
444 Raise KeyError if there if there is no message with that UID.
445 """
446 (code, data) = self._server.uid('fetch', uid, "(BODY.PEEK[HEADER])")
447 if code != 'OK':
448 raise ImapNotOkError, "%s in fetch_header(%s)" % (code, uid)
449 try:
450 rfc822string = data[0][1]
451 except TypeError:
452 raise KeyError, "No UID %s in get_header" % uid
453 result = ImapMessage(rfc822string)
454 result.set_imapflags(self.get_imapflags(uid))
455 result.internaldate = self.get_internaldate(uid)
456 result.size = self.get_size(uid)
457 if self._factory is ImapMessage:
458 return result
459 return self._factory(result)
460
462 """ Return an mailbox.Message object containing only the requested
463 header fields of the message with UID.
464 The fields parameter is a string ofheader fields seperated by
465 spaces, e.g. 'From SUBJECT date'
466 Raise KeyError if there if there is no message with that UID.
467 """
468 (code, data) = self._server.uid('fetch', uid,
469 "(BODY.PEEK[HEADER.FIELDS (%s)])"
470 % fields)
471 if code != 'OK':
472 raise ImapNotOkError, "%s in fetch_header(%s)" % (code, uid)
473 try:
474 rfc822string = data[0][1]
475 except TypeError:
476 raise KeyError, "No UID %s in get_fields" % uid
477 result = Message(rfc822string)
478 return result
479
480
482 """ Get the number of bytes contained in the message with UID """
483 try:
484 (code, data) = self._server.uid('fetch', uid, '(RFC822.SIZE)')
485 sizeresult = data[0]
486 if code != 'OK':
487 raise ImapNotOkError, "%s in get_imapflags(%s)" % (code, uid)
488 if sizeresult is None:
489 raise NoSuchUIDError, "No message %s in get_size" % uid
490 startindex = sizeresult.find('SIZE') + 5
491 stopindex = sizeresult.find(' ', startindex)
492 return int(sizeresult[startindex:stopindex])
493 except (TypeError, ValueError):
494 raise ValueError, "Unexpected results while fetching flags " \
495 + "from server for message %s" % uid
496
498 """ Return a list of imap flags for the message with UID
499 Raise exception if there if there is no message with that UID.
500 """
501 try:
502 (code, data) = self._server.uid('fetch', uid, '(FLAGS)')
503 flagresult = data[0]
504 if code != 'OK':
505 raise ImapNotOkError, "%s in get_imapflags(%s)" % (code, uid)
506 return list(imaplib.ParseFlags(flagresult))
507 except (TypeError, ValueError):
508 raise ValueError, "Unexpected results while fetching flags " \
509 + "from server for message %s; response was (%s, %s)" \
510 % (uid, code, data)
511
513 """ Return a time tuple representing the internal date for the
514 message with UID
515 Raise exception if there if there is no message with that UID.
516 """
517 try:
518 (code, data) = self._server.uid('fetch', uid, '(INTERNALDATE)')
519 dateresult = data[0]
520 if code != 'OK':
521 raise ImapNotOkError, "%s in get_internaldate(%s)" % (code, uid)
522 if dateresult is None:
523 raise NoSuchUIDError, "No message %s in get_internaldate" % uid
524 return imaplib.Internaldate2tuple(dateresult)
525 except (TypeError, ValueError):
526 raise ValueError, "Unexpected results while fetching flags " \
527 + "from server for message %s" % uid
528
529
531 """ Equality test:
532 mailboxes are equal if they are equal in server and name
533 """
534 if not isinstance(other, ImapMailbox):
535 return False
536 return ( (self._server == other.server) \
537 and (self.name == other.name) \
538 )
539
541 """ Inequality test:
542 mailboxes are unequal if they are not equal
543 """
544 return (not (self == other))
545
546 - def copy(self, uid, targetmailbox, exact=False):
547 """ Copy the message with UID to the targetmailbox and try to return
548 the key that was assigned to the copied message in the
549 targetmailbox. If targetmailbox is an ImapMailbox, this is
550 the target-UID.
551 targetmailbox can be a string (the name of a mailbox on the
552 same imap server), any of mailbox.Mailbox. Note that not all
553 imap flags will be preserved if the targetmailbox is not on
554 an ImapMailbox. Copying is efficient (i.e. the message is not
555 downloaded) if the targetmailbox is on the same server.
556 Do nothing and return None if there if there is no message with
557 that UID.
558 Unless 'exact' is set to True, the return value will be None if
559 the targetmailbox is an ImapMailbox. This is because finding out
560 the new UID of the copied message on an IMAP server is non-trivial.
561 Giving 'exact' as True means that additional work will be done to
562 find the accurate result. This operation can be relatively
563 expensive. If targetmailbox is not an ImapMailbox, the value of
564 'exact' is irrelevant, and the return value will always be
565 accurate.
566 """
567 result = None
568 if isinstance(targetmailbox, ImapMailbox):
569 if targetmailbox.server == self._server:
570 targetmailbox = targetmailbox.name
571 if isinstance(targetmailbox, Mailbox):
572 if self != targetmailbox:
573 targetmailbox.lock()
574 result = targetmailbox.add(self[uid])
575 if isinstance(targetmailbox, ImapMailbox):
576 result = None
577 targetmailbox.flush()
578 if exact:
579 pass
580
581 targetmailbox.unlock()
582 elif isinstance(targetmailbox, str):
583 if targetmailbox != self.name:
584 (code, data) = self._server.uid('copy', uid, targetmailbox)
585 if code != 'OK':
586 raise ImapNotOkError, "%s in copy: %s" % (code, data)
587 if exact:
588 pass
589
590
591 else:
592 return uid
593 else:
594 raise TypeError, "targetmailbox in copy is of unknown type."
595 return result
596
597
598
599 - def move(self, uid, targetmailbox, exact=False):
600 """ Copy the message with UID to the targetmailbox, delete it in the
601 original mailbox, and try to return the key that was assigned to
602 the copied message in the targetmailbox.
603 The discussions of the copy method concerning 'targetmailbox' and
604 'exact' apply here as well.
605 Do nothing and return None if there if there is no message with that UID.
606 """
607 result = None
608 if self.readonly:
609 raise ReadOnlyError, "Tried to move message from read-only mailbox"
610 if (targetmailbox != self) and (targetmailbox != self.name):
611 result = self.copy(uid, targetmailbox, exact)
612 (code, data) = self._server.uid('store', uid, \
613 '+FLAGS', "(\\Deleted)")
614 if code != 'OK':
615 raise ImapNotOkError, "%s in move: %s" % (code, data)
616 else:
617 return uid
618 return result
619
620
621 - def discard(self, uid, exact=False):
622 """ If trash folder is defined, move the message with UID to
623 trash and try to return the key assigned to the message in the
624 trash; else, just add the \Deleted flag to the message with UID and
625 return None.
626 If a trash folder is defined, this method is equivalent to
627 self.move(uid, self.trash). The discussions of the move/copy method
628 apply.
629 Do nothing and return None if there if there is no message with
630 that UID.
631 """
632 result = None
633 if self.readonly:
634 raise ReadOnlyError, "Tried to discard from read-only mailbox"
635 if self.trash is None:
636 self.add_imapflag(uid, "\\Deleted")
637 else:
638 print "Moving to %s" % self.trash
639 return self.move(uid, self.trash, exact)
640 return result
641
642 - def remove(self, uid, exact=False):
643 """ Discard the message with UID.
644 If there is no message with that UID, raise a KeyError
645 This is exactly equivalent to self.discard(uid), except for
646 the KeyError exception.
647 """
648 if self.readonly:
649 raise ReadOnlyError, "Tried to remove from read-only mailbox"
650 if uid not in self.search("ALL"):
651 raise KeyError, "No UID %s" % uid
652 return self.discard(uid, exact)
653
655 """ Discard the message with UID.
656 If there is no message with that UID, raise a KeyError
657 """
658 self.remove(uid)
659
661 """ Replace the message corresponding to key with message.
662 This operation is not supported for IMAP mailboxes
663 and will raise NotSupportedError
664 """
665 raise NotSupportedError, "Setting items in IMAP not supported"
666
668 """ Return an iterator over all UIDs
669 This is an iterator over the list of UIDs at the time iterkeys()
670 is a called.
671 """
672 return iter(self.search("ALL"))
673
675 """ Return a list of all UIDs """
676 return self.search("ALL")
677
679 """ Return an iterator over all messages. The messages are
680 represented as instances of ImapMessage unless a custom message
681 factory was specified when the Mailbox instance was initialized.
682 """
683 for uid in self.search("ALL"):
684 yield self[uid]
685
687 """ Return an iterator over all messages.
688 Identical to itervalues
689 """
690 return self.itervalues()
691
693 """ Return a list of all messages
694 The messages are represented as instances of ImapMessage unless
695 a custom message factory was specified when the Mailbox instance
696 was initialized.
697 Beware that this method can be extremely expensive in terms
698 of time, bandwidth, and memory.
699 """
700 messagelist = []
701 for message in self:
702 messagelist.append(message)
703 return messagelist
704
706 """ Return an iterator over (uid, message) pairs,
707 where uid is a key and message is a message representation.
708 """
709 for uid in self.keys():
710 yield((uid, self[uid]))
711
713 """ Return a list (uid, message) pairs,
714 where uid is a key and message is a message representation.
715 Beware that this method can be extremely expensive in terms
716 of time, bandwidth, and memory.
717 """
718 result = []
719 for uid in self.keys():
720 result.append((uid, self[uid]))
721 return result
722
723 - def add(self, message):
724 """ Add the message to mailbox.
725 Message can be an instance of email.Message.Message
726 (including instaces of mailbox.Message and its subclasses );
727 or an open file handle or a string containing an RFC822 message.
728 Return the highest UID in the mailbox, which should be, but
729 is not guaranteed to be, the UID of the message that was added.
730 Raise ImapNotOkError if a non-OK response is received from
731 the server
732 """
733 if self.readonly:
734 raise ReadOnlyError, "Tried to add to a read-only mailbox"
735 message = ImapMessage(message)
736 flags = message.flagstring()
737 date_time = message.internaldatestring()
738 memoryfile = StringIO()
739 generator = Generator(memoryfile, mangle_from_=False)
740 generator.flatten(message)
741 message_str = memoryfile.getvalue()
742 (code, data) = self._server.append(self.name, flags, \
743 date_time, message_str)
744 if code != 'OK':
745 raise ImapNotOkError, "%s in add: %s" % (code, data)
746 try:
747 return self.get_all_uids()[-1]
748 except IndexError:
749 return 0
750
751
753 """ Add imap flag to message with UID.
754 """
755 if self.readonly:
756 raise ReadOnlyError, \
757 "Tried to add imap flag for message in read-only mailbox"
758 for flag in flags:
759 (code, data) = self._server.uid('store', uid, '+FLAGS', \
760 "(%s)" % flag )
761 if code != 'OK':
762 raise ImapNotOkError, "%s in add_flags(%s, %s): %s" \
763 % (uid, flag, code, data)
764
766 """ Remove imap flags from message with UID
767 """
768 if self.readonly:
769 raise ReadOnlyError, \
770 "Tried to remove imap flag from message in read-only mailbox"
771 for flag in flags:
772 (code, data) = self._server.uid('store', uid, '-FLAGS', \
773 "(%s)" % flag )
774 if code != 'OK':
775 raise ImapNotOkError, "%s in remove_flag(%s, %s): %s" \
776 % (uid, flag, code, data)
777
779 """ Set imap flags for message with UID
780 flags must be an iterable of flags, or a string.
781 If flags is a string, it is taken as the single flag
782 to be set.
783 """
784 if self.readonly:
785 raise ReadOnlyError, \
786 "Tried to set imap flags for message in read-only mailbox"
787 if isinstance(flags, str):
788 flags = [flags]
789 flagstring = "(%s)" % ' '.join(flags)
790 (code, data) = self._server.uid('store', uid, 'FLAGS', flagstring )
791 if code != 'OK':
792 raise ImapNotOkError, "%s in set_imapflags(%s, %s): %s" \
793 % (code, uid, flags, data)
794
796 """ Flush mailbox, close connection to server """
797 self.flush()
798 self._server.close()
799 self._server.logout()
800 if hasattr(self._server, 'locked'):
801 del self._server.locked
802
804 """ Expunge the mailbox (delete all messages marked for deletion)"""
805 if self.readonly:
806 raise ReadOnlyError, "Tried to expunge read-only mailbox"
807 self._server.expunge()
808