]>
code.delx.au - offlineimap/blob - offlineimap/imaplib.py
7 Public functions: Internaldate2tuple
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16 # String method conversion by ESR, February 2001.
17 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
18 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
19 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
20 # IMAP4_Tunnel contributed by John Goerzen <jgoerzen@complete.org> July 2002
24 import binascii
, re
, socket
, time
, random
, sys
, os
25 from offlineimap
.ui
import UIBase
27 __all__
= ["IMAP4", "Internaldate2tuple",
28 "Int2AP", "ParseFlags", "Time2Internaldate"]
36 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
42 'APPEND': ('AUTH', 'SELECTED'),
43 'AUTHENTICATE': ('NONAUTH',),
44 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
45 'CHECK': ('SELECTED',),
46 'CLOSE': ('SELECTED',),
47 'COPY': ('SELECTED',),
48 'CREATE': ('AUTH', 'SELECTED'),
49 'DELETE': ('AUTH', 'SELECTED'),
50 'EXAMINE': ('AUTH', 'SELECTED'),
51 'EXPUNGE': ('SELECTED',),
52 'FETCH': ('SELECTED',),
53 'GETACL': ('AUTH', 'SELECTED'),
54 'GETQUOTA': ('AUTH', 'SELECTED'),
55 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
56 'LIST': ('AUTH', 'SELECTED'),
57 'LOGIN': ('NONAUTH',),
58 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
59 'LSUB': ('AUTH', 'SELECTED'),
60 'NAMESPACE': ('AUTH', 'SELECTED'),
61 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
62 'PARTIAL': ('SELECTED',), # NB: obsolete
63 'RENAME': ('AUTH', 'SELECTED'),
64 'SEARCH': ('SELECTED',),
65 'SELECT': ('AUTH', 'SELECTED'),
66 'SETACL': ('AUTH', 'SELECTED'),
67 'SETQUOTA': ('AUTH', 'SELECTED'),
68 'SORT': ('SELECTED',),
69 'STATUS': ('AUTH', 'SELECTED'),
70 'STORE': ('SELECTED',),
71 'SUBSCRIBE': ('AUTH', 'SELECTED'),
73 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
76 # Patterns to match server responses
78 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
79 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
80 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
81 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
82 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
83 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
85 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
86 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
87 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
88 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
94 """IMAP4 client class.
96 Instantiate with: IMAP4([host[, port]])
98 host - host's name (default: localhost);
99 port - port number (default: standard IMAP4 port).
101 All IMAP4rev1 commands are supported by methods of the same
102 name (in lower-case).
104 All arguments to commands are converted to strings, except for
105 AUTHENTICATE, and the last argument to APPEND which is passed as
106 an IMAP4 literal. If necessary (the string contains any
107 non-printing characters or white-space and isn't enclosed with
108 either parentheses or double quotes) each string is quoted.
109 However, the 'password' argument to the LOGIN command is always
110 quoted. If you want to avoid having an argument string quoted
111 (eg: the 'flags' argument to STORE) then enclose the string in
112 parentheses (eg: "(\Deleted)").
114 Each command returns a tuple: (type, [data, ...]) where 'type'
115 is usually 'OK' or 'NO', and 'data' is either the text from the
116 tagged response, or untagged results from command.
118 Errors raise the exception class <instance>.error("<reason>").
119 IMAP4 server errors raise <instance>.abort("<reason>"),
120 which is a sub-class of 'error'. Mailbox status changes
121 from READ-WRITE to READ-ONLY raise the exception class
122 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
124 "error" exceptions imply a program error.
125 "abort" exceptions imply the connection should be reset, and
126 the command re-tried.
127 "readonly" exceptions imply the command should be re-tried.
129 Note: to use this module, you must read the RFCs pertaining
130 to the IMAP4 protocol, as the semantics of the arguments to
131 each IMAP4 command are left to the invoker, not to mention
135 class error(Exception): pass # Logical errors - debug required
136 class abort(error
): pass # Service errors - close and retry
137 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
139 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
141 def __init__(self
, host
= '', port
= IMAP4_PORT
):
143 self
.state
= 'LOGOUT'
144 self
.literal
= None # A literal argument to a command
145 self
.tagged_commands
= {} # Tagged commands awaiting response
146 self
.untagged_responses
= {} # {typ: [data, ...], ...}
147 self
.continuation_response
= '' # Last continuation response
148 self
.is_readonly
= None # READ-ONLY desired state
151 # Open socket to server.
153 self
.open(host
, port
)
155 # Create unique tag for this session,
156 # and compile tagged response matcher.
158 self
.tagpre
= Int2AP(random
.randint(0, 31999))
159 self
.tagre
= re
.compile(r
'(?P<tag>'
161 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
163 # Get server welcome message,
164 # request and store CAPABILITY response.
167 self
._cmd
_log
_len
= 10
168 self
._cmd
_log
_idx
= 0
169 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
171 self
._mesg
('imaplib version %s' % __version__
)
172 self
._mesg
('new IMAP4 connection, tag=%s' % self
.tagpre
)
174 self
.welcome
= self
._get
_response
()
175 if 'PREAUTH' in self
.untagged_responses
:
177 elif 'OK' in self
.untagged_responses
:
178 self
.state
= 'NONAUTH'
180 raise self
.error(self
.welcome
)
183 self
._simple
_command
(cap
)
184 if not cap
in self
.untagged_responses
:
185 raise self
.error('no CAPABILITY response from server')
186 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
190 self
._mesg
('CAPABILITIES: %s' % `self
.capabilities`
)
192 for version
in AllowedVersions
:
193 if not version
in self
.capabilities
:
195 self
.PROTOCOL_VERSION
= version
198 raise self
.error('server not IMAP4 compliant')
201 def __getattr__(self
, attr
):
202 # Allow UPPERCASE variants of IMAP4 command methods.
204 return getattr(self
, attr
.lower())
205 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
209 # Overridable methods
212 def open(self
, host
= '', port
= IMAP4_PORT
):
213 """Setup connection to remote server on "host:port"
214 (default: localhost:standard IMAP4 port).
215 This connection will be used by the routines:
216 read, readline, send, shutdown.
220 #This connects to the first ip found ipv4/ipv6
221 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
222 #example from the python documentation:
223 #http://www.python.org/doc/lib/socket-example.html
224 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
226 af
, socktype
, proto
, canonname
, sa
= res
[0]
227 self
.sock
= socket
.socket(af
, socktype
, proto
)
228 self
.sock
.connect(sa
)
230 self
.file = self
.sock
.makefile('rb')
233 def read(self
, size
):
234 """Read 'size' bytes from remote."""
236 while len(retval
) < size
:
237 retval
+= self
.file.read(size
- len(retval
))
241 """Read line from remote."""
242 return self
.file.readline()
245 def send(self
, data
):
246 """Send data to remote."""
247 self
.sock
.sendall(data
)
251 """Close I/O established in "open"."""
257 """Return socket instance used to connect to IMAP4 server.
259 socket = <instance>.socket()
269 """Return most recent 'RECENT' responses if any exist,
270 else prompt server for an update using the 'NOOP' command.
272 (typ, [data]) = <instance>.recent()
274 'data' is None if no new messages,
275 else list of RECENT responses, most recent last.
278 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
281 typ
, dat
= self
.noop() # Prod server for response
282 return self
._untagged
_response
(typ
, dat
, name
)
285 def response(self
, code
):
286 """Return data for response 'code' if received, or None.
288 Old value for response 'code' is cleared.
290 (code, [data]) = <instance>.response(code)
292 return self
._untagged
_response
(code
, [None], code
.upper())
299 def append(self
, mailbox
, flags
, date_time
, message
):
300 """Append message to named mailbox.
302 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
304 All args except `message' can be None.
310 if (flags
[0],flags
[-1]) != ('(',')'):
311 flags
= '(%s)' % flags
315 date_time
= Time2Internaldate(date_time
)
318 self
.literal
= message
319 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
322 def authenticate(self
, mechanism
, authobject
):
323 """Authenticate command - requires response processing.
325 'mechanism' specifies which authentication mechanism is to
326 be used - it must appear in <instance>.capabilities in the
327 form AUTH=<mechanism>.
329 'authobject' must be a callable object:
331 data = authobject(response)
333 It will be called to process server continuation responses.
334 It should return data that will be encoded and sent to server.
335 It should return None if the client abort response '*' should
338 mech
= mechanism
.upper()
339 cap
= 'AUTH=%s' % mech
340 if not cap
in self
.capabilities
:
341 raise self
.error("Server doesn't allow %s authentication." % mech
)
342 self
.literal
= _Authenticator(authobject
).process
343 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
345 raise self
.error(dat
[-1])
351 """Checkpoint mailbox on server.
353 (typ, [data]) = <instance>.check()
355 return self
._simple
_command
('CHECK')
359 """Close currently selected mailbox.
361 Deleted messages are removed from writable mailbox.
362 This is the recommended command before 'LOGOUT'.
364 (typ, [data]) = <instance>.close()
367 typ
, dat
= self
._simple
_command
('CLOSE')
373 def copy(self
, message_set
, new_mailbox
):
374 """Copy 'message_set' messages onto end of 'new_mailbox'.
376 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
378 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
381 def create(self
, mailbox
):
382 """Create new mailbox.
384 (typ, [data]) = <instance>.create(mailbox)
386 return self
._simple
_command
('CREATE', mailbox
)
389 def delete(self
, mailbox
):
390 """Delete old mailbox.
392 (typ, [data]) = <instance>.delete(mailbox)
394 return self
._simple
_command
('DELETE', mailbox
)
398 """Permanently remove deleted items from selected mailbox.
400 Generates 'EXPUNGE' response for each deleted message.
402 (typ, [data]) = <instance>.expunge()
404 'data' is list of 'EXPUNGE'd message numbers in order received.
407 typ
, dat
= self
._simple
_command
(name
)
408 return self
._untagged
_response
(typ
, dat
, name
)
411 def fetch(self
, message_set
, message_parts
):
412 """Fetch (parts of) messages.
414 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
416 'message_parts' should be a string of selected parts
417 enclosed in parentheses, eg: "(UID BODY[TEXT])".
419 'data' are tuples of message part envelope and data.
422 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
423 return self
._untagged
_response
(typ
, dat
, name
)
426 def getacl(self
, mailbox
):
427 """Get the ACLs for a mailbox.
429 (typ, [data]) = <instance>.getacl(mailbox)
431 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
432 return self
._untagged
_response
(typ
, dat
, 'ACL')
435 def getquota(self
, root
):
436 """Get the quota root's resource usage and limits.
438 Part of the IMAP4 QUOTA extension defined in rfc2087.
440 (typ, [data]) = <instance>.getquota(root)
442 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
443 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
446 def getquotaroot(self
, mailbox
):
447 """Get the list of quota roots for the named mailbox.
449 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
451 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
452 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
453 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
454 return typ
, [quotaroot
, quota
]
457 def list(self
, directory
='""', pattern
='*'):
458 """List mailbox names in directory matching pattern.
460 (typ, [data]) = <instance>.list(directory='""', pattern='*')
462 'data' is list of LIST responses.
465 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
466 return self
._untagged
_response
(typ
, dat
, name
)
469 def login(self
, user
, password
):
470 """Identify client using plaintext password.
472 (typ, [data]) = <instance>.login(user, password)
474 NB: 'password' will be quoted.
476 #if not 'AUTH=LOGIN' in self.capabilities:
477 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
478 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
480 raise self
.error(dat
[-1])
486 """Shutdown connection to server.
488 (typ, [data]) = <instance>.logout()
490 Returns server 'BYE' response.
492 self
.state
= 'LOGOUT'
493 try: typ
, dat
= self
._simple
_command
('LOGOUT')
494 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
496 if 'BYE' in self
.untagged_responses
:
497 return 'BYE', self
.untagged_responses
['BYE']
501 def lsub(self
, directory
='""', pattern
='*'):
502 """List 'subscribed' mailbox names in directory matching pattern.
504 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
506 'data' are tuples of message part envelope and data.
509 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
510 return self
._untagged
_response
(typ
, dat
, name
)
514 """ Returns IMAP namespaces ala rfc2342
516 (typ, [data, ...]) = <instance>.namespace()
519 typ
, dat
= self
._simple
_command
(name
)
520 return self
._untagged
_response
(typ
, dat
, name
)
524 """Send NOOP command.
526 (typ, data) = <instance>.noop()
530 self
._dump
_ur
(self
.untagged_responses
)
531 return self
._simple
_command
('NOOP')
534 def partial(self
, message_num
, message_part
, start
, length
):
535 """Fetch truncated part of a message.
537 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
539 'data' is tuple of message part envelope and data.
542 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
543 return self
._untagged
_response
(typ
, dat
, 'FETCH')
546 def rename(self
, oldmailbox
, newmailbox
):
547 """Rename old mailbox name to new.
549 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
551 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
554 def search(self
, charset
, *criteria
):
555 """Search mailbox for matching messages.
557 (typ, [data]) = <instance>.search(charset, criterium, ...)
559 'data' is space separated list of matching message numbers.
563 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
565 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
566 return self
._untagged
_response
(typ
, dat
, name
)
569 def select(self
, mailbox
='INBOX', readonly
=None):
572 Flush all untagged responses.
574 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
576 'data' is count of messages in mailbox ('EXISTS' response).
578 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
579 self
.untagged_responses
= {} # Flush old responses.
580 self
.is_readonly
= readonly
582 typ
, dat
= self
._simple
_command
(name
, mailbox
)
584 self
.state
= 'AUTH' # Might have been 'SELECTED'
586 self
.state
= 'SELECTED'
587 if 'READ-ONLY' in self
.untagged_responses \
591 self
._dump
_ur
(self
.untagged_responses
)
592 raise self
.readonly('%s is not writable' % mailbox
)
593 return typ
, self
.untagged_responses
.get('EXISTS', [None])
596 def setacl(self
, mailbox
, who
, what
):
597 """Set a mailbox acl.
599 (typ, [data]) = <instance>.create(mailbox, who, what)
601 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
604 def setquota(self
, root
, limits
):
605 """Set the quota root's resource limits.
607 (typ, [data]) = <instance>.setquota(root, limits)
609 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
610 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
613 def sort(self
, sort_criteria
, charset
, *search_criteria
):
614 """IMAP4rev1 extension SORT command.
616 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
619 #if not name in self.capabilities: # Let the server decide!
620 # raise self.error('unimplemented extension command: %s' % name)
621 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
622 sort_criteria
= '(%s)' % sort_criteria
623 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
624 return self
._untagged
_response
(typ
, dat
, name
)
627 def status(self
, mailbox
, names
):
628 """Request named status conditions for mailbox.
630 (typ, [data]) = <instance>.status(mailbox, names)
633 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
634 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
635 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
636 return self
._untagged
_response
(typ
, dat
, name
)
639 def store(self
, message_set
, command
, flags
):
640 """Alters flag dispositions for messages in mailbox.
642 (typ, [data]) = <instance>.store(message_set, command, flags)
644 if (flags
[0],flags
[-1]) != ('(',')'):
645 flags
= '(%s)' % flags
# Avoid quoting the flags
646 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
647 return self
._untagged
_response
(typ
, dat
, 'FETCH')
650 def subscribe(self
, mailbox
):
651 """Subscribe to new mailbox.
653 (typ, [data]) = <instance>.subscribe(mailbox)
655 return self
._simple
_command
('SUBSCRIBE', mailbox
)
658 def uid(self
, command
, *args
):
659 """Execute "command arg ..." with messages identified by UID,
660 rather than message number.
662 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
664 Returns response appropriate to 'command'.
666 command
= command
.upper()
667 if not command
in Commands
:
668 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
669 if self
.state
not in Commands
[command
]:
670 raise self
.error('command %s illegal in state %s'
671 % (command
, self
.state
))
673 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
674 if command
in ('SEARCH', 'SORT'):
678 return self
._untagged
_response
(typ
, dat
, name
)
681 def unsubscribe(self
, mailbox
):
682 """Unsubscribe from old mailbox.
684 (typ, [data]) = <instance>.unsubscribe(mailbox)
686 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
689 def xatom(self
, name
, *args
):
690 """Allow simple extension commands
691 notified by server in CAPABILITY response.
693 Assumes command is legal in current state.
695 (typ, [data]) = <instance>.xatom(name, arg, ...)
697 Returns response appropriate to extension command `name'.
700 #if not name in self.capabilities: # Let the server decide!
701 # raise self.error('unknown extension command: %s' % name)
702 if not name
in Commands
:
703 Commands
[name
] = (self
.state
,)
704 return apply(self
._simple
_command
, (name
,) + args
)
711 def _append_untagged(self
, typ
, dat
):
713 if dat
is None: dat
= ''
714 ur
= self
.untagged_responses
717 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
718 (typ
, len(ur
.get(typ
,'')), dat
))
725 def _check_bye(self
):
726 bye
= self
.untagged_responses
.get('BYE')
728 raise self
.abort(bye
[-1])
731 def _command(self
, name
, *args
):
733 if self
.state
not in Commands
[name
]:
736 'command %s illegal in state %s' % (name
, self
.state
))
738 for typ
in ('OK', 'NO', 'BAD'):
739 if typ
in self
.untagged_responses
:
740 del self
.untagged_responses
[typ
]
742 if 'READ-ONLY' in self
.untagged_responses \
743 and not self
.is_readonly
:
744 raise self
.readonly('mailbox status changed to READ-ONLY')
746 tag
= self
._new
_tag
()
747 data
= '%s %s' % (tag
, name
)
749 if arg
is None: continue
750 data
= '%s %s' % (data
, self
._checkquote
(arg
))
752 literal
= self
.literal
753 if literal
is not None:
755 if type(literal
) is type(self
._command
):
759 data
= '%s {%s}' % (data
, len(literal
))
763 self
._mesg
('> %s' % data
)
765 self
._log
('> %s' % data
)
768 self
.send('%s%s' % (data
, CRLF
))
769 except (socket
.error
, OSError), val
:
770 raise self
.abort('socket error: %s' % val
)
776 # Wait for continuation response
778 while self
._get
_response
():
779 if self
.tagged_commands
[tag
]: # BAD/NO?
785 literal
= literator(self
.continuation_response
)
789 self
._mesg
('write literal size %s' % len(literal
))
794 except (socket
.error
, OSError), val
:
795 raise self
.abort('socket error: %s' % val
)
803 def _command_complete(self
, name
, tag
):
806 typ
, data
= self
._get
_tagged
_response
(tag
)
807 except self
.abort
, val
:
808 raise self
.abort('command: %s => %s' % (name
, val
))
809 except self
.error
, val
:
810 raise self
.error('command: %s => %s' % (name
, val
))
813 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
817 def _get_response(self
):
819 # Read response and store.
821 # Returns None for continuation responses,
822 # otherwise first response line received.
824 resp
= self
._get
_line
()
826 # Command completion response?
828 if self
._match
(self
.tagre
, resp
):
829 tag
= self
.mo
.group('tag')
830 if not tag
in self
.tagged_commands
:
831 raise self
.abort('unexpected tagged response: %s' % resp
)
833 typ
= self
.mo
.group('type')
834 dat
= self
.mo
.group('data')
835 self
.tagged_commands
[tag
] = (typ
, [dat
])
839 # '*' (untagged) responses?
841 if not self
._match
(Untagged_response
, resp
):
842 if self
._match
(Untagged_status
, resp
):
843 dat2
= self
.mo
.group('data2')
846 # Only other possibility is '+' (continuation) response...
848 if self
._match
(Continuation
, resp
):
849 self
.continuation_response
= self
.mo
.group('data')
850 return None # NB: indicates continuation
852 raise self
.abort("unexpected response: '%s'" % resp
)
854 typ
= self
.mo
.group('type')
855 dat
= self
.mo
.group('data')
856 if dat
is None: dat
= '' # Null untagged response
857 if dat2
: dat
= dat
+ ' ' + dat2
859 # Is there a literal to come?
861 while self
._match
(Literal
, dat
):
863 # Read literal direct from connection.
865 size
= int(self
.mo
.group('size'))
868 self
._mesg
('read literal size %s' % size
)
869 data
= self
.read(size
)
871 # Store response with literal as tuple
873 self
._append
_untagged
(typ
, (dat
, data
))
875 # Read trailer - possibly containing another literal
877 dat
= self
._get
_line
()
879 self
._append
_untagged
(typ
, dat
)
881 # Bracketed response information?
883 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
884 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
887 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
888 self
._mesg
('%s response: %s' % (typ
, dat
))
893 def _get_tagged_response(self
, tag
):
896 result
= self
.tagged_commands
[tag
]
897 if result
is not None:
898 del self
.tagged_commands
[tag
]
901 # Some have reported "unexpected response" exceptions.
902 # Note that ignoring them here causes loops.
903 # Instead, send me details of the unexpected response and
904 # I'll update the code in `_get_response()'.
908 except self
.abort
, val
:
917 line
= self
.readline()
919 raise self
.abort('socket error: EOF')
921 # Protocol mandates all lines terminated by CRLF
926 self
._mesg
('< %s' % line
)
928 self
._log
('< %s' % line
)
932 def _match(self
, cre
, s
):
934 # Run compiled regular expression match method on 's'.
935 # Save result, return success.
937 self
.mo
= cre
.match(s
)
939 if self
.mo
is not None and self
.debug
>= 5:
940 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
941 return self
.mo
is not None
946 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
947 self
.tagnum
= self
.tagnum
+ 1
948 self
.tagged_commands
[tag
] = None
952 def _checkquote(self
, arg
):
954 # Must quote command args if non-alphanumeric chars present,
955 # and not already quoted.
957 if type(arg
) is not type(''):
959 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
961 if self
.mustquote
.search(arg
) is None:
963 return self
._quote
(arg
)
966 def _quote(self
, arg
):
968 arg
= arg
.replace('\\', '\\\\')
969 arg
= arg
.replace('"', '\\"')
974 def _simple_command(self
, name
, *args
):
976 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
979 def _untagged_response(self
, typ
, dat
, name
):
983 if not name
in self
.untagged_responses
:
985 data
= self
.untagged_responses
[name
]
988 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
989 del self
.untagged_responses
[name
]
995 def _mesg(self
, s
, secs
=None):
998 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
999 UIBase
.getglobalui().debug('imap', ' %s.%02d %s' % (tm
, (secs
*100)%100, s
))
1001 def _dump_ur(self
, dict):
1002 # Dump untagged responses (in `dict').
1006 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1007 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1009 def _log(self
, line
):
1010 # Keep log of last `_cmd_log_len' interactions for debugging.
1011 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1012 self
._cmd
_log
_idx
+= 1
1013 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1014 self
._cmd
_log
_idx
= 0
1016 def print_log(self
):
1017 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1018 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1021 apply(self
._mesg
, self
._cmd
_log
[i
])
1025 if i
>= self
._cmd
_log
_len
:
1029 class IMAP4_Tunnel(IMAP4
):
1030 """IMAP4 client class over a tunnel
1032 Instantiate with: IMAP4_Tunnel(tunnelcmd)
1034 tunnelcmd -- shell command to generate the tunnel.
1035 The result will be in PREAUTH stage."""
1037 def __init__(self
, tunnelcmd
):
1038 IMAP4
.__init__(self
, tunnelcmd
)
1040 def open(self
, host
, port
):
1041 """The tunnelcmd comes in on host!"""
1042 self
.outfd
, self
.infd
= os
.popen2(host
, "t", 0)
1044 def read(self
, size
):
1046 while len(retval
) < size
:
1047 retval
+= self
.infd
.read(size
- len(retval
))
1051 return self
.infd
.readline()
1053 def send(self
, data
):
1054 self
.outfd
.write(data
)
1062 def __init__(self
, sslsock
):
1063 self
.sslsock
= sslsock
1067 return self
.sslsock
.write(s
)
1070 return self
.sslsock
.read(n
)
1073 if len(self
.readbuf
):
1074 # Return the stuff in readbuf, even if less than n.
1075 # It might contain the rest of the line, and if we try to
1076 # read more, might block waiting for data that is not
1078 bytesfrombuf
= min(n
, len(self
.readbuf
))
1079 retval
= self
.readbuf
[:bytesfrombuf
]
1080 self
.readbuf
= self
.readbuf
[bytesfrombuf
:]
1082 retval
= self
._read
(n
)
1084 self
.readbuf
= retval
[n
:]
1091 linebuf
= self
.read(1024)
1092 nlindex
= linebuf
.find("\n")
1094 retval
+= linebuf
[:nlindex
+ 1]
1095 self
.readbuf
= linebuf
[nlindex
+ 1:] + self
.readbuf
1101 class IMAP4_SSL(IMAP4
):
1103 """IMAP4 client class over SSL connection
1105 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1107 host - host's name (default: localhost);
1108 port - port number (default: standard IMAP4 SSL port).
1109 keyfile - PEM formatted file that contains your private key (default: None);
1110 certfile - PEM formatted certificate chain file (default: None);
1112 for more documentation see the docstring of the parent class IMAP4.
1116 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1117 self
.keyfile
= keyfile
1118 self
.certfile
= certfile
1119 IMAP4
.__init__(self
, host
, port
)
1122 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1123 """Setup connection to remote server on "host:port".
1124 (default: localhost:standard IMAP4 SSL port).
1125 This connection will be used by the routines:
1126 read, readline, send, shutdown.
1130 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
1131 self
.sock
.connect((host
, port
))
1132 if sys
.version_info
[0] <= 2 and sys
.version_info
[1] <= 2:
1133 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1135 self
.sslobj
= socket
.ssl(self
.sock
._sock
, self
.keyfile
, self
.certfile
)
1136 self
.sslobj
= sslwrapper(self
.sslobj
)
1139 def read(self
, size
):
1140 """Read 'size' bytes from remote."""
1142 while len(retval
) < size
:
1143 retval
+= self
.sslobj
.read(size
- len(retval
))
1148 """Read line from remote."""
1149 return self
.sslobj
.readline()
1151 def send(self
, data
):
1152 """Send data to remote."""
1154 bytestowrite
= len(data
)
1155 while byteswritten
< bytestowrite
:
1156 byteswritten
+= self
.sslobj
.write(data
[byteswritten
:])
1160 """Close I/O established in "open"."""
1165 """Return socket instance used to connect to IMAP4 server.
1167 socket = <instance>.socket()
1173 """Return SSLObject instance used to communicate with the IMAP4 server.
1175 ssl = <instance>.socket.ssl()
1181 class _Authenticator
:
1183 """Private class to provide en/decoding
1184 for base64-based authentication conversation.
1187 def __init__(self
, mechinst
):
1188 self
.mech
= mechinst
# Callable object to provide/process data
1190 def process(self
, data
):
1191 ret
= self
.mech(self
.decode(data
))
1193 return '*' # Abort conversation
1194 return self
.encode(ret
)
1196 def encode(self
, inp
):
1198 # Invoke binascii.b2a_base64 iteratively with
1199 # short even length buffers, strip the trailing
1200 # line feed from the result and append. "Even"
1201 # means a number that factors to both 6 and 8,
1202 # so when it gets to the end of the 8-bit input
1203 # there's no partial 6-bit output.
1213 e
= binascii
.b2a_base64(t
)
1218 def decode(self
, inp
):
1221 return binascii
.a2b_base64(inp
)
1225 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1226 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1228 def Internaldate2tuple(resp
):
1229 """Convert IMAP4 INTERNALDATE to UT.
1231 Returns Python time module tuple.
1234 mo
= InternalDate
.match(resp
)
1238 mon
= Mon2num
[mo
.group('mon')]
1239 zonen
= mo
.group('zonen')
1241 day
= int(mo
.group('day'))
1242 year
= int(mo
.group('year'))
1243 hour
= int(mo
.group('hour'))
1244 min = int(mo
.group('min'))
1245 sec
= int(mo
.group('sec'))
1246 zoneh
= int(mo
.group('zoneh'))
1247 zonem
= int(mo
.group('zonem'))
1249 # INTERNALDATE timezone must be subtracted to get UT
1251 zone
= (zoneh
*60 + zonem
)*60
1255 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1257 utc
= time
.mktime(tt
)
1259 # Following is necessary because the time module has no 'mkgmtime'.
1260 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1262 lt
= time
.localtime(utc
)
1263 if time
.daylight
and lt
[-1]:
1264 zone
= zone
+ time
.altzone
1266 zone
= zone
+ time
.timezone
1268 return time
.localtime(utc
- zone
)
1274 """Convert integer to A-P string representation."""
1276 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1279 num
, mod
= divmod(num
, 16)
1285 def ParseFlags(resp
):
1287 """Convert IMAP4 flags response to python tuple."""
1289 mo
= Flags
.match(resp
)
1293 return tuple(mo
.group('flags').split())
1296 def Time2Internaldate(date_time
):
1298 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1300 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1303 if isinstance(date_time
, (int, float)):
1304 tt
= time
.localtime(date_time
)
1305 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1307 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1308 return date_time
# Assume in correct format
1310 raise ValueError("date_time not of a known type")
1312 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1315 if time
.daylight
and tt
[-1]:
1316 zone
= -time
.altzone
1318 zone
= -time
.timezone
1319 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1323 if __name__
== '__main__':
1325 import getopt
, getpass
1328 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1329 except getopt
.error
, val
:
1332 for opt
,val
in optlist
:
1336 if not args
: args
= ('',)
1340 USER
= getpass
.getuser()
1341 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1343 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1345 ('login', (USER
, PASSWD
)),
1346 ('create', ('/tmp/xxx 1',)),
1347 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1348 ('CREATE', ('/tmp/yyz 2',)),
1349 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1350 ('list', ('/tmp', 'yy*')),
1351 ('select', ('/tmp/yyz 2',)),
1352 ('search', (None, 'SUBJECT', 'test')),
1353 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1354 ('store', ('1', 'FLAGS', '(\Deleted)')),
1363 ('response',('UIDVALIDITY',)),
1364 ('uid', ('SEARCH', 'ALL')),
1365 ('response', ('EXISTS',)),
1366 ('append', (None, None, None, test_mesg
)),
1372 M
._mesg
('%s %s' % (cmd
, args
))
1373 typ
, dat
= apply(getattr(M
, cmd
), args
)
1374 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1379 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1380 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1382 for cmd
,args
in test_seq1
:
1385 for ml
in run('list', ('/tmp/', 'yy%')):
1386 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1387 if mo
: path
= mo
.group(1)
1388 else: path
= ml
.split()[-1]
1389 run('delete', (path
,))
1391 for cmd
,args
in test_seq2
:
1392 dat
= run(cmd
, args
)
1394 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1397 uid
= dat
[-1].split()
1398 if not uid
: continue
1399 run('uid', ('FETCH', '%s' % uid
[-1],
1400 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1402 print '\nAll tests OK.'
1405 print '\nTests failed.'
1409 If you would like to see debugging output,