]>
code.delx.au - offlineimap/blob - offlineimap/imaplib.py
7 Public functions: Internaldate2tuple
14 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
16 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
17 # String method conversion by ESR, February 2001.
18 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
19 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
20 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
21 # IMAP4_Tunnel contributed by John Goerzen <jgoerzen@complete.org> July 2002
25 import binascii
, re
, socket
, time
, random
, subprocess
, sys
, os
26 from offlineimap
.ui
import UIBase
28 __all__
= ["IMAP4", "Internaldate2tuple", "Internaldate2epoch",
29 "Int2AP", "ParseFlags", "Time2Internaldate"]
37 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
43 'APPEND': ('AUTH', 'SELECTED'),
44 'AUTHENTICATE': ('NONAUTH',),
45 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
46 'CHECK': ('SELECTED',),
47 'CLOSE': ('SELECTED',),
48 'COPY': ('SELECTED',),
49 'CREATE': ('AUTH', 'SELECTED'),
50 'DELETE': ('AUTH', 'SELECTED'),
51 'EXAMINE': ('AUTH', 'SELECTED'),
52 'EXPUNGE': ('SELECTED',),
53 'FETCH': ('SELECTED',),
54 'GETACL': ('AUTH', 'SELECTED'),
55 'GETQUOTA': ('AUTH', 'SELECTED'),
56 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
57 'LIST': ('AUTH', 'SELECTED'),
58 'LOGIN': ('NONAUTH',),
59 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
60 'LSUB': ('AUTH', 'SELECTED'),
61 'NAMESPACE': ('AUTH', 'SELECTED'),
62 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
63 'PARTIAL': ('SELECTED',), # NB: obsolete
64 'RENAME': ('AUTH', 'SELECTED'),
65 'SEARCH': ('SELECTED',),
66 'SELECT': ('AUTH', 'SELECTED'),
67 'SETACL': ('AUTH', 'SELECTED'),
68 'SETQUOTA': ('AUTH', 'SELECTED'),
69 'SORT': ('SELECTED',),
70 'STATUS': ('AUTH', 'SELECTED'),
71 'STORE': ('SELECTED',),
72 'SUBSCRIBE': ('AUTH', 'SELECTED'),
74 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
77 # Patterns to match server responses
79 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
80 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
81 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
82 r
'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
83 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
84 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
86 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
87 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
88 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
89 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
95 """IMAP4 client class.
97 Instantiate with: IMAP4([host[, port]])
99 host - host's name (default: localhost);
100 port - port number (default: standard IMAP4 port).
102 All IMAP4rev1 commands are supported by methods of the same
103 name (in lower-case).
105 All arguments to commands are converted to strings, except for
106 AUTHENTICATE, and the last argument to APPEND which is passed as
107 an IMAP4 literal. If necessary (the string contains any
108 non-printing characters or white-space and isn't enclosed with
109 either parentheses or double quotes) each string is quoted.
110 However, the 'password' argument to the LOGIN command is always
111 quoted. If you want to avoid having an argument string quoted
112 (eg: the 'flags' argument to STORE) then enclose the string in
113 parentheses (eg: "(\Deleted)").
115 Each command returns a tuple: (type, [data, ...]) where 'type'
116 is usually 'OK' or 'NO', and 'data' is either the text from the
117 tagged response, or untagged results from command.
119 Errors raise the exception class <instance>.error("<reason>").
120 IMAP4 server errors raise <instance>.abort("<reason>"),
121 which is a sub-class of 'error'. Mailbox status changes
122 from READ-WRITE to READ-ONLY raise the exception class
123 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
125 "error" exceptions imply a program error.
126 "abort" exceptions imply the connection should be reset, and
127 the command re-tried.
128 "readonly" exceptions imply the command should be re-tried.
130 Note: to use this module, you must read the RFCs pertaining
131 to the IMAP4 protocol, as the semantics of the arguments to
132 each IMAP4 command are left to the invoker, not to mention
136 class error(Exception): pass # Logical errors - debug required
137 class abort(error
): pass # Service errors - close and retry
138 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
140 mustquote
= re
.compile(r
"[^\w!#$%&'+,.:;<=>?^`|~-]")
142 def __init__(self
, host
= '', port
= IMAP4_PORT
):
144 self
.state
= 'LOGOUT'
145 self
.literal
= None # A literal argument to a command
146 self
.tagged_commands
= {} # Tagged commands awaiting response
147 self
.untagged_responses
= {} # {typ: [data, ...], ...}
148 self
.continuation_response
= '' # Last continuation response
149 self
.is_readonly
= None # READ-ONLY desired state
152 # Open socket to server.
154 self
.open(host
, port
)
156 # Create unique tag for this session,
157 # and compile tagged response matcher.
159 self
.tagpre
= Int2AP(random
.randint(0, 31999))
160 self
.tagre
= re
.compile(r
'(?P<tag>'
162 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
164 # Get server welcome message,
165 # request and store CAPABILITY response.
168 self
._cmd
_log
_len
= 10
169 self
._cmd
_log
_idx
= 0
170 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
172 self
._mesg
('imaplib version %s' % __version__
)
173 self
._mesg
('new IMAP4 connection, tag=%s' % self
.tagpre
)
175 self
.welcome
= self
._get
_response
()
176 if 'PREAUTH' in self
.untagged_responses
:
178 elif 'OK' in self
.untagged_responses
:
179 self
.state
= 'NONAUTH'
181 raise self
.error(self
.welcome
)
184 self
._simple
_command
(cap
)
185 if not cap
in self
.untagged_responses
:
186 raise self
.error('no CAPABILITY response from server')
187 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
191 self
._mesg
('CAPABILITIES: %s' % `self
.capabilities`
)
193 for version
in AllowedVersions
:
194 if not version
in self
.capabilities
:
196 self
.PROTOCOL_VERSION
= version
199 raise self
.error('server not IMAP4 compliant')
202 def __getattr__(self
, attr
):
203 # Allow UPPERCASE variants of IMAP4 command methods.
205 return getattr(self
, attr
.lower())
206 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
210 # Overridable methods
213 def open(self
, host
= '', port
= IMAP4_PORT
):
214 """Setup connection to remote server on "host:port"
215 (default: localhost:standard IMAP4 port).
216 This connection will be used by the routines:
217 read, readline, send, shutdown.
221 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
223 self
.sock
= socket
.socket(af
, socktype
, proto
)
225 # Try each address returned by getaddrinfo in turn until we
226 # manage to connect to one.
227 # Try all the addresses in turn until we connect()
230 af
, socktype
, proto
, canonname
, sa
= remote
231 self
.sock
= socket
.socket(af
, socktype
, proto
)
232 last_error
= self
.sock
.connect_ex(sa
)
239 raise socket
.error(last_error
)
240 self
.file = self
.sock
.makefile('rb')
242 def read(self
, size
):
243 """Read 'size' bytes from remote."""
245 while len(retval
) < size
:
246 retval
+= self
.file.read(size
- len(retval
))
250 """Read line from remote."""
251 return self
.file.readline()
254 def send(self
, data
):
255 """Send data to remote."""
256 self
.sock
.sendall(data
)
260 """Close I/O established in "open"."""
266 """Return socket instance used to connect to IMAP4 server.
268 socket = <instance>.socket()
278 """Return most recent 'RECENT' responses if any exist,
279 else prompt server for an update using the 'NOOP' command.
281 (typ, [data]) = <instance>.recent()
283 'data' is None if no new messages,
284 else list of RECENT responses, most recent last.
287 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
290 typ
, dat
= self
.noop() # Prod server for response
291 return self
._untagged
_response
(typ
, dat
, name
)
294 def response(self
, code
):
295 """Return data for response 'code' if received, or None.
297 Old value for response 'code' is cleared.
299 (code, [data]) = <instance>.response(code)
301 return self
._untagged
_response
(code
, [None], code
.upper())
308 def append(self
, mailbox
, flags
, date_time
, message
):
309 """Append message to named mailbox.
311 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
313 All args except `message' can be None.
319 if (flags
[0],flags
[-1]) != ('(',')'):
320 flags
= '(%s)' % flags
324 date_time
= Time2Internaldate(date_time
)
327 self
.literal
= message
328 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
331 def authenticate(self
, mechanism
, authobject
):
332 """Authenticate command - requires response processing.
334 'mechanism' specifies which authentication mechanism is to
335 be used - it must appear in <instance>.capabilities in the
336 form AUTH=<mechanism>.
338 'authobject' must be a callable object:
340 data = authobject(response)
342 It will be called to process server continuation responses.
343 It should return data that will be encoded and sent to server.
344 It should return None if the client abort response '*' should
347 mech
= mechanism
.upper()
348 cap
= 'AUTH=%s' % mech
349 if not cap
in self
.capabilities
:
350 raise self
.error("Server doesn't allow %s authentication." % mech
)
351 self
.literal
= _Authenticator(authobject
).process
352 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
354 raise self
.error(dat
[-1])
360 """Checkpoint mailbox on server.
362 (typ, [data]) = <instance>.check()
364 return self
._simple
_command
('CHECK')
368 """Close currently selected mailbox.
370 Deleted messages are removed from writable mailbox.
371 This is the recommended command before 'LOGOUT'.
373 (typ, [data]) = <instance>.close()
376 typ
, dat
= self
._simple
_command
('CLOSE')
382 def copy(self
, message_set
, new_mailbox
):
383 """Copy 'message_set' messages onto end of 'new_mailbox'.
385 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
387 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
390 def create(self
, mailbox
):
391 """Create new mailbox.
393 (typ, [data]) = <instance>.create(mailbox)
395 return self
._simple
_command
('CREATE', mailbox
)
398 def delete(self
, mailbox
):
399 """Delete old mailbox.
401 (typ, [data]) = <instance>.delete(mailbox)
403 return self
._simple
_command
('DELETE', mailbox
)
407 """Permanently remove deleted items from selected mailbox.
409 Generates 'EXPUNGE' response for each deleted message.
411 (typ, [data]) = <instance>.expunge()
413 'data' is list of 'EXPUNGE'd message numbers in order received.
416 typ
, dat
= self
._simple
_command
(name
)
417 return self
._untagged
_response
(typ
, dat
, name
)
420 def fetch(self
, message_set
, message_parts
):
421 """Fetch (parts of) messages.
423 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
425 'message_parts' should be a string of selected parts
426 enclosed in parentheses, eg: "(UID BODY[TEXT])".
428 'data' are tuples of message part envelope and data.
431 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
432 return self
._untagged
_response
(typ
, dat
, name
)
435 def getacl(self
, mailbox
):
436 """Get the ACLs for a mailbox.
438 (typ, [data]) = <instance>.getacl(mailbox)
440 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
441 return self
._untagged
_response
(typ
, dat
, 'ACL')
444 def getquota(self
, root
):
445 """Get the quota root's resource usage and limits.
447 Part of the IMAP4 QUOTA extension defined in rfc2087.
449 (typ, [data]) = <instance>.getquota(root)
451 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
452 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
455 def getquotaroot(self
, mailbox
):
456 """Get the list of quota roots for the named mailbox.
458 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
460 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
461 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
462 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
463 return typ
, [quotaroot
, quota
]
466 def list(self
, directory
='""', pattern
='*'):
467 """List mailbox names in directory matching pattern.
469 (typ, [data]) = <instance>.list(directory='""', pattern='*')
471 'data' is list of LIST responses.
474 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
475 return self
._untagged
_response
(typ
, dat
, name
)
478 def login(self
, user
, password
):
479 """Identify client using plaintext password.
481 (typ, [data]) = <instance>.login(user, password)
483 NB: 'password' will be quoted.
485 #if not 'AUTH=LOGIN' in self.capabilities:
486 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
487 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
489 raise self
.error(dat
[-1])
495 """Shutdown connection to server.
497 (typ, [data]) = <instance>.logout()
499 Returns server 'BYE' response.
501 self
.state
= 'LOGOUT'
502 try: typ
, dat
= self
._simple
_command
('LOGOUT')
503 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
505 if 'BYE' in self
.untagged_responses
:
506 return 'BYE', self
.untagged_responses
['BYE']
510 def lsub(self
, directory
='""', pattern
='*'):
511 """List 'subscribed' mailbox names in directory matching pattern.
513 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
515 'data' are tuples of message part envelope and data.
518 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
519 return self
._untagged
_response
(typ
, dat
, name
)
523 """ Returns IMAP namespaces ala rfc2342
525 (typ, [data, ...]) = <instance>.namespace()
528 typ
, dat
= self
._simple
_command
(name
)
529 return self
._untagged
_response
(typ
, dat
, name
)
533 """Send NOOP command.
535 (typ, data) = <instance>.noop()
539 self
._dump
_ur
(self
.untagged_responses
)
540 return self
._simple
_command
('NOOP')
543 def partial(self
, message_num
, message_part
, start
, length
):
544 """Fetch truncated part of a message.
546 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
548 'data' is tuple of message part envelope and data.
551 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
552 return self
._untagged
_response
(typ
, dat
, 'FETCH')
555 def rename(self
, oldmailbox
, newmailbox
):
556 """Rename old mailbox name to new.
558 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
560 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
563 def search(self
, charset
, *criteria
):
564 """Search mailbox for matching messages.
566 (typ, [data]) = <instance>.search(charset, criterium, ...)
568 'data' is space separated list of matching message numbers.
572 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
574 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
575 return self
._untagged
_response
(typ
, dat
, name
)
578 def select(self
, mailbox
='INBOX', readonly
=None):
581 Flush all untagged responses.
583 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
585 'data' is count of messages in mailbox ('EXISTS' response).
587 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
588 self
.untagged_responses
= {} # Flush old responses.
589 self
.is_readonly
= readonly
591 typ
, dat
= self
._simple
_command
(name
, mailbox
)
593 self
.state
= 'AUTH' # Might have been 'SELECTED'
595 self
.state
= 'SELECTED'
596 if 'READ-ONLY' in self
.untagged_responses \
600 self
._dump
_ur
(self
.untagged_responses
)
601 raise self
.readonly('%s is not writable' % mailbox
)
602 return typ
, self
.untagged_responses
.get('EXISTS', [None])
605 def setacl(self
, mailbox
, who
, what
):
606 """Set a mailbox acl.
608 (typ, [data]) = <instance>.create(mailbox, who, what)
610 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
613 def setquota(self
, root
, limits
):
614 """Set the quota root's resource limits.
616 (typ, [data]) = <instance>.setquota(root, limits)
618 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
619 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
622 def sort(self
, sort_criteria
, charset
, *search_criteria
):
623 """IMAP4rev1 extension SORT command.
625 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
628 #if not name in self.capabilities: # Let the server decide!
629 # raise self.error('unimplemented extension command: %s' % name)
630 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
631 sort_criteria
= '(%s)' % sort_criteria
632 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
633 return self
._untagged
_response
(typ
, dat
, name
)
636 def status(self
, mailbox
, names
):
637 """Request named status conditions for mailbox.
639 (typ, [data]) = <instance>.status(mailbox, names)
642 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
643 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
644 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
645 return self
._untagged
_response
(typ
, dat
, name
)
648 def store(self
, message_set
, command
, flags
):
649 """Alters flag dispositions for messages in mailbox.
651 (typ, [data]) = <instance>.store(message_set, command, flags)
653 if (flags
[0],flags
[-1]) != ('(',')'):
654 flags
= '(%s)' % flags
# Avoid quoting the flags
655 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
656 return self
._untagged
_response
(typ
, dat
, 'FETCH')
659 def subscribe(self
, mailbox
):
660 """Subscribe to new mailbox.
662 (typ, [data]) = <instance>.subscribe(mailbox)
664 return self
._simple
_command
('SUBSCRIBE', mailbox
)
667 def uid(self
, command
, *args
):
668 """Execute "command arg ..." with messages identified by UID,
669 rather than message number.
671 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
673 Returns response appropriate to 'command'.
675 command
= command
.upper()
676 if not command
in Commands
:
677 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
678 if self
.state
not in Commands
[command
]:
679 raise self
.error('command %s illegal in state %s'
680 % (command
, self
.state
))
682 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
683 if command
in ('SEARCH', 'SORT'):
687 return self
._untagged
_response
(typ
, dat
, name
)
690 def unsubscribe(self
, mailbox
):
691 """Unsubscribe from old mailbox.
693 (typ, [data]) = <instance>.unsubscribe(mailbox)
695 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
698 def xatom(self
, name
, *args
):
699 """Allow simple extension commands
700 notified by server in CAPABILITY response.
702 Assumes command is legal in current state.
704 (typ, [data]) = <instance>.xatom(name, arg, ...)
706 Returns response appropriate to extension command `name'.
709 #if not name in self.capabilities: # Let the server decide!
710 # raise self.error('unknown extension command: %s' % name)
711 if not name
in Commands
:
712 Commands
[name
] = (self
.state
,)
713 return apply(self
._simple
_command
, (name
,) + args
)
720 def _append_untagged(self
, typ
, dat
):
722 if dat
is None: dat
= ''
723 ur
= self
.untagged_responses
726 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
727 (typ
, len(ur
.get(typ
,'')), dat
))
734 def _check_bye(self
):
735 bye
= self
.untagged_responses
.get('BYE')
737 raise self
.abort(bye
[-1])
740 def _command(self
, name
, *args
):
742 if self
.state
not in Commands
[name
]:
745 'command %s illegal in state %s' % (name
, self
.state
))
747 for typ
in ('OK', 'NO', 'BAD'):
748 if typ
in self
.untagged_responses
:
749 del self
.untagged_responses
[typ
]
751 if 'READ-ONLY' in self
.untagged_responses \
752 and not self
.is_readonly
:
753 raise self
.readonly('mailbox status changed to READ-ONLY')
755 tag
= self
._new
_tag
()
756 data
= '%s %s' % (tag
, name
)
758 if arg
is None: continue
759 data
= '%s %s' % (data
, self
._checkquote
(arg
))
761 literal
= self
.literal
762 if literal
is not None:
764 if type(literal
) is type(self
._command
):
768 data
= '%s {%s}' % (data
, len(literal
))
772 self
._mesg
('> %s' % data
)
774 self
._log
('> %s' % data
)
777 self
.send('%s%s' % (data
, CRLF
))
778 except (socket
.error
, OSError), val
:
779 raise self
.abort('socket error: %s' % val
)
785 # Wait for continuation response
787 while self
._get
_response
():
788 if self
.tagged_commands
[tag
]: # BAD/NO?
794 literal
= literator(self
.continuation_response
)
798 self
._mesg
('write literal size %s' % len(literal
))
803 except (socket
.error
, OSError), val
:
804 raise self
.abort('socket error: %s' % val
)
812 def _command_complete(self
, name
, tag
):
815 typ
, data
= self
._get
_tagged
_response
(tag
)
816 except self
.abort
, val
:
817 raise self
.abort('command: %s => %s' % (name
, val
))
818 except self
.error
, val
:
819 raise self
.error('command: %s => %s' % (name
, val
))
822 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
826 def _get_response(self
):
828 # Read response and store.
830 # Returns None for continuation responses,
831 # otherwise first response line received.
833 resp
= self
._get
_line
()
835 # Command completion response?
837 if self
._match
(self
.tagre
, resp
):
838 tag
= self
.mo
.group('tag')
839 if not tag
in self
.tagged_commands
:
840 raise self
.abort('unexpected tagged response: %s' % resp
)
842 typ
= self
.mo
.group('type')
843 dat
= self
.mo
.group('data')
844 self
.tagged_commands
[tag
] = (typ
, [dat
])
848 # '*' (untagged) responses?
850 if not self
._match
(Untagged_response
, resp
):
851 if self
._match
(Untagged_status
, resp
):
852 dat2
= self
.mo
.group('data2')
855 # Only other possibility is '+' (continuation) response...
857 if self
._match
(Continuation
, resp
):
858 self
.continuation_response
= self
.mo
.group('data')
859 return None # NB: indicates continuation
861 raise self
.abort("unexpected response: '%s'" % resp
)
863 typ
= self
.mo
.group('type')
864 dat
= self
.mo
.group('data')
865 if dat
is None: dat
= '' # Null untagged response
866 if dat2
: dat
= dat
+ ' ' + dat2
868 # Is there a literal to come?
870 while self
._match
(Literal
, dat
):
872 # Read literal direct from connection.
874 size
= int(self
.mo
.group('size'))
877 self
._mesg
('read literal size %s' % size
)
878 data
= self
.read(size
)
880 # Store response with literal as tuple
882 self
._append
_untagged
(typ
, (dat
, data
))
884 # Read trailer - possibly containing another literal
886 dat
= self
._get
_line
()
888 self
._append
_untagged
(typ
, dat
)
890 # Bracketed response information?
892 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
893 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
896 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
897 self
._mesg
('%s response: %s' % (typ
, dat
))
902 def _get_tagged_response(self
, tag
):
905 result
= self
.tagged_commands
[tag
]
906 if result
is not None:
907 del self
.tagged_commands
[tag
]
910 # Some have reported "unexpected response" exceptions.
911 # Note that ignoring them here causes loops.
912 # Instead, send me details of the unexpected response and
913 # I'll update the code in `_get_response()'.
917 except self
.abort
, val
:
926 line
= self
.readline()
928 raise self
.abort('socket error: EOF')
930 # Protocol mandates all lines terminated by CRLF
935 self
._mesg
('< %s' % line
)
937 self
._log
('< %s' % line
)
941 def _match(self
, cre
, s
):
943 # Run compiled regular expression match method on 's'.
944 # Save result, return success.
946 self
.mo
= cre
.match(s
)
948 if self
.mo
is not None and self
.debug
>= 5:
949 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
950 return self
.mo
is not None
955 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
956 self
.tagnum
= self
.tagnum
+ 1
957 self
.tagged_commands
[tag
] = None
961 def _checkquote(self
, arg
):
963 # Must quote command args if non-alphanumeric chars present,
964 # and not already quoted.
966 if type(arg
) is not type(''):
968 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
970 if self
.mustquote
.search(arg
) is None:
972 return self
._quote
(arg
)
975 def _quote(self
, arg
):
977 arg
= arg
.replace('\\', '\\\\')
978 arg
= arg
.replace('"', '\\"')
983 def _simple_command(self
, name
, *args
):
985 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
988 def _untagged_response(self
, typ
, dat
, name
):
992 if not name
in self
.untagged_responses
:
994 data
= self
.untagged_responses
[name
]
997 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
998 del self
.untagged_responses
[name
]
1004 def _mesg(self
, s
, secs
=None):
1007 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1008 UIBase
.getglobalui().debug('imap', ' %s.%02d %s' % (tm
, (secs
*100)%100, s
))
1010 def _dump_ur(self
, dict):
1011 # Dump untagged responses (in `dict').
1015 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1016 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1018 def _log(self
, line
):
1019 # Keep log of last `_cmd_log_len' interactions for debugging.
1020 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1021 self
._cmd
_log
_idx
+= 1
1022 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1023 self
._cmd
_log
_idx
= 0
1025 def print_log(self
):
1026 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1027 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1030 apply(self
._mesg
, self
._cmd
_log
[i
])
1034 if i
>= self
._cmd
_log
_len
:
1038 class IMAP4_Tunnel(IMAP4
):
1039 """IMAP4 client class over a tunnel
1041 Instantiate with: IMAP4_Tunnel(tunnelcmd)
1043 tunnelcmd -- shell command to generate the tunnel.
1044 The result will be in PREAUTH stage."""
1046 def __init__(self
, tunnelcmd
):
1047 IMAP4
.__init__(self
, tunnelcmd
)
1049 def open(self
, host
, port
):
1050 """The tunnelcmd comes in on host!"""
1051 self
.process
= subprocess
.Popen(host
, shell
=True, close_fds
=True,
1052 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
)
1053 (self
.outfd
, self
.infd
) = (self
.process
.stdin
, self
.process
.stdout
)
1055 def read(self
, size
):
1057 while len(retval
) < size
:
1058 retval
+= self
.infd
.read(size
- len(retval
))
1062 return self
.infd
.readline()
1064 def send(self
, data
):
1065 self
.outfd
.write(data
)
1074 def __init__(self
, sslsock
):
1075 self
.sslsock
= sslsock
1079 return self
.sslsock
.write(s
)
1082 return self
.sslsock
.read(n
)
1085 if len(self
.readbuf
):
1086 # Return the stuff in readbuf, even if less than n.
1087 # It might contain the rest of the line, and if we try to
1088 # read more, might block waiting for data that is not
1090 bytesfrombuf
= min(n
, len(self
.readbuf
))
1091 retval
= self
.readbuf
[:bytesfrombuf
]
1092 self
.readbuf
= self
.readbuf
[bytesfrombuf
:]
1094 retval
= self
._read
(n
)
1096 self
.readbuf
= retval
[n
:]
1103 linebuf
= self
.read(1024)
1104 nlindex
= linebuf
.find("\n")
1106 retval
+= linebuf
[:nlindex
+ 1]
1107 self
.readbuf
= linebuf
[nlindex
+ 1:] + self
.readbuf
1113 class IMAP4_SSL(IMAP4
):
1115 """IMAP4 client class over SSL connection
1117 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1119 host - host's name (default: localhost);
1120 port - port number (default: standard IMAP4 SSL port).
1121 keyfile - PEM formatted file that contains your private key (default: None);
1122 certfile - PEM formatted certificate chain file (default: None);
1124 for more documentation see the docstring of the parent class IMAP4.
1128 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1129 self
.keyfile
= keyfile
1130 self
.certfile
= certfile
1131 IMAP4
.__init__(self
, host
, port
)
1134 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1135 """Setup connection to remote server on "host:port".
1136 (default: localhost:standard IMAP4 SSL port).
1137 This connection will be used by the routines:
1138 read, readline, send, shutdown.
1142 #This connects to the first ip found ipv4/ipv6
1143 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
1144 #example from the python documentation:
1145 #http://www.python.org/doc/lib/socket-example.html
1146 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
1148 # Try all the addresses in turn until we connect()
1151 af
, socktype
, proto
, canonname
, sa
= remote
1152 self
.sock
= socket
.socket(af
, socktype
, proto
)
1153 last_error
= self
.sock
.connect_ex(sa
)
1160 raise socket
.error(last_error
)
1161 if sys
.version_info
[0] <= 2 and sys
.version_info
[1] <= 2:
1162 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1164 self
.sslobj
= socket
.ssl(self
.sock
._sock
, self
.keyfile
, self
.certfile
)
1165 self
.sslobj
= sslwrapper(self
.sslobj
)
1168 def read(self
, size
):
1169 """Read 'size' bytes from remote."""
1171 while len(retval
) < size
:
1172 retval
+= self
.sslobj
.read(size
- len(retval
))
1177 """Read line from remote."""
1178 return self
.sslobj
.readline()
1180 def send(self
, data
):
1181 """Send data to remote."""
1183 bytestowrite
= len(data
)
1184 while byteswritten
< bytestowrite
:
1185 byteswritten
+= self
.sslobj
.write(data
[byteswritten
:])
1189 """Close I/O established in "open"."""
1194 """Return socket instance used to connect to IMAP4 server.
1196 socket = <instance>.socket()
1202 """Return SSLObject instance used to communicate with the IMAP4 server.
1204 ssl = <instance>.socket.ssl()
1210 class _Authenticator
:
1212 """Private class to provide en/decoding
1213 for base64-based authentication conversation.
1216 def __init__(self
, mechinst
):
1217 self
.mech
= mechinst
# Callable object to provide/process data
1219 def process(self
, data
):
1220 ret
= self
.mech(self
.decode(data
))
1222 return '*' # Abort conversation
1223 return self
.encode(ret
)
1225 def encode(self
, inp
):
1227 # Invoke binascii.b2a_base64 iteratively with
1228 # short even length buffers, strip the trailing
1229 # line feed from the result and append. "Even"
1230 # means a number that factors to both 6 and 8,
1231 # so when it gets to the end of the 8-bit input
1232 # there's no partial 6-bit output.
1242 e
= binascii
.b2a_base64(t
)
1247 def decode(self
, inp
):
1250 return binascii
.a2b_base64(inp
)
1254 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1255 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1257 def Internaldate2epoch(resp
):
1258 """Convert IMAP4 INTERNALDATE to UT.
1260 Returns seconds since the epoch.
1263 mo
= InternalDate
.match(resp
)
1267 mon
= Mon2num
[mo
.group('mon')]
1268 zonen
= mo
.group('zonen')
1270 day
= int(mo
.group('day'))
1271 year
= int(mo
.group('year'))
1272 hour
= int(mo
.group('hour'))
1273 min = int(mo
.group('min'))
1274 sec
= int(mo
.group('sec'))
1275 zoneh
= int(mo
.group('zoneh'))
1276 zonem
= int(mo
.group('zonem'))
1278 # INTERNALDATE timezone must be subtracted to get UT
1280 zone
= (zoneh
*60 + zonem
)*60
1284 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1286 return time
.mktime(tt
)
1289 def Internaldate2tuple(resp
):
1290 """Convert IMAP4 INTERNALDATE to UT.
1292 Returns Python time module tuple.
1295 utc
= Internaldate2epoch(resp
)
1297 # Following is necessary because the time module has no 'mkgmtime'.
1298 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1300 lt
= time
.localtime(utc
)
1301 if time
.daylight
and lt
[-1]:
1302 zone
= zone
+ time
.altzone
1304 zone
= zone
+ time
.timezone
1306 return time
.localtime(utc
- zone
)
1312 """Convert integer to A-P string representation."""
1314 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1317 num
, mod
= divmod(num
, 16)
1323 def ParseFlags(resp
):
1325 """Convert IMAP4 flags response to python tuple."""
1327 mo
= Flags
.match(resp
)
1331 return tuple(mo
.group('flags').split())
1334 def Time2Internaldate(date_time
):
1336 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1338 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1341 if isinstance(date_time
, (int, float)):
1342 tt
= time
.localtime(date_time
)
1343 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1345 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1346 return date_time
# Assume in correct format
1348 raise ValueError("date_time not of a known type")
1350 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1353 if time
.daylight
and tt
[-1]:
1354 zone
= -time
.altzone
1356 zone
= -time
.timezone
1357 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1361 if __name__
== '__main__':
1363 import getopt
, getpass
1366 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1367 except getopt
.error
, val
:
1370 for opt
,val
in optlist
:
1374 if not args
: args
= ('',)
1378 USER
= getpass
.getuser()
1379 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1381 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1383 ('login', (USER
, PASSWD
)),
1384 ('create', ('/tmp/xxx 1',)),
1385 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1386 ('CREATE', ('/tmp/yyz 2',)),
1387 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1388 ('list', ('/tmp', 'yy*')),
1389 ('select', ('/tmp/yyz 2',)),
1390 ('search', (None, 'SUBJECT', 'test')),
1391 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1392 ('store', ('1', 'FLAGS', '(\Deleted)')),
1401 ('response',('UIDVALIDITY',)),
1402 ('uid', ('SEARCH', 'ALL')),
1403 ('response', ('EXISTS',)),
1404 ('append', (None, None, None, test_mesg
)),
1410 M
._mesg
('%s %s' % (cmd
, args
))
1411 typ
, dat
= apply(getattr(M
, cmd
), args
)
1412 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1417 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1418 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1420 for cmd
,args
in test_seq1
:
1423 for ml
in run('list', ('/tmp/', 'yy%')):
1424 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1425 if mo
: path
= mo
.group(1)
1426 else: path
= ml
.split()[-1]
1427 run('delete', (path
,))
1429 for cmd
,args
in test_seq2
:
1430 dat
= run(cmd
, args
)
1432 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1435 uid
= dat
[-1].split()
1436 if not uid
: continue
1437 run('uid', ('FETCH', '%s' % uid
[-1],
1438 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1440 print '\nAll tests OK.'
1443 print '\nTests failed.'
1447 If you would like to see debugging output,