]>
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
)
229 self
.file = self
.sock
.makefile('rb')
231 def read(self
, size
):
232 """Read 'size' bytes from remote."""
234 while len(retval
) < size
:
235 retval
+= self
.file.read(size
- len(retval
))
239 """Read line from remote."""
240 return self
.file.readline()
243 def send(self
, data
):
244 """Send data to remote."""
245 self
.sock
.sendall(data
)
249 """Close I/O established in "open"."""
255 """Return socket instance used to connect to IMAP4 server.
257 socket = <instance>.socket()
267 """Return most recent 'RECENT' responses if any exist,
268 else prompt server for an update using the 'NOOP' command.
270 (typ, [data]) = <instance>.recent()
272 'data' is None if no new messages,
273 else list of RECENT responses, most recent last.
276 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
279 typ
, dat
= self
.noop() # Prod server for response
280 return self
._untagged
_response
(typ
, dat
, name
)
283 def response(self
, code
):
284 """Return data for response 'code' if received, or None.
286 Old value for response 'code' is cleared.
288 (code, [data]) = <instance>.response(code)
290 return self
._untagged
_response
(code
, [None], code
.upper())
297 def append(self
, mailbox
, flags
, date_time
, message
):
298 """Append message to named mailbox.
300 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
302 All args except `message' can be None.
308 if (flags
[0],flags
[-1]) != ('(',')'):
309 flags
= '(%s)' % flags
313 date_time
= Time2Internaldate(date_time
)
316 self
.literal
= message
317 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
320 def authenticate(self
, mechanism
, authobject
):
321 """Authenticate command - requires response processing.
323 'mechanism' specifies which authentication mechanism is to
324 be used - it must appear in <instance>.capabilities in the
325 form AUTH=<mechanism>.
327 'authobject' must be a callable object:
329 data = authobject(response)
331 It will be called to process server continuation responses.
332 It should return data that will be encoded and sent to server.
333 It should return None if the client abort response '*' should
336 mech
= mechanism
.upper()
337 cap
= 'AUTH=%s' % mech
338 if not cap
in self
.capabilities
:
339 raise self
.error("Server doesn't allow %s authentication." % mech
)
340 self
.literal
= _Authenticator(authobject
).process
341 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
343 raise self
.error(dat
[-1])
349 """Checkpoint mailbox on server.
351 (typ, [data]) = <instance>.check()
353 return self
._simple
_command
('CHECK')
357 """Close currently selected mailbox.
359 Deleted messages are removed from writable mailbox.
360 This is the recommended command before 'LOGOUT'.
362 (typ, [data]) = <instance>.close()
365 typ
, dat
= self
._simple
_command
('CLOSE')
371 def copy(self
, message_set
, new_mailbox
):
372 """Copy 'message_set' messages onto end of 'new_mailbox'.
374 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
376 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
379 def create(self
, mailbox
):
380 """Create new mailbox.
382 (typ, [data]) = <instance>.create(mailbox)
384 return self
._simple
_command
('CREATE', mailbox
)
387 def delete(self
, mailbox
):
388 """Delete old mailbox.
390 (typ, [data]) = <instance>.delete(mailbox)
392 return self
._simple
_command
('DELETE', mailbox
)
396 """Permanently remove deleted items from selected mailbox.
398 Generates 'EXPUNGE' response for each deleted message.
400 (typ, [data]) = <instance>.expunge()
402 'data' is list of 'EXPUNGE'd message numbers in order received.
405 typ
, dat
= self
._simple
_command
(name
)
406 return self
._untagged
_response
(typ
, dat
, name
)
409 def fetch(self
, message_set
, message_parts
):
410 """Fetch (parts of) messages.
412 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
414 'message_parts' should be a string of selected parts
415 enclosed in parentheses, eg: "(UID BODY[TEXT])".
417 'data' are tuples of message part envelope and data.
420 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
421 return self
._untagged
_response
(typ
, dat
, name
)
424 def getacl(self
, mailbox
):
425 """Get the ACLs for a mailbox.
427 (typ, [data]) = <instance>.getacl(mailbox)
429 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
430 return self
._untagged
_response
(typ
, dat
, 'ACL')
433 def getquota(self
, root
):
434 """Get the quota root's resource usage and limits.
436 Part of the IMAP4 QUOTA extension defined in rfc2087.
438 (typ, [data]) = <instance>.getquota(root)
440 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
441 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
444 def getquotaroot(self
, mailbox
):
445 """Get the list of quota roots for the named mailbox.
447 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
449 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
450 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
451 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
452 return typ
, [quotaroot
, quota
]
455 def list(self
, directory
='""', pattern
='*'):
456 """List mailbox names in directory matching pattern.
458 (typ, [data]) = <instance>.list(directory='""', pattern='*')
460 'data' is list of LIST responses.
463 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
464 return self
._untagged
_response
(typ
, dat
, name
)
467 def login(self
, user
, password
):
468 """Identify client using plaintext password.
470 (typ, [data]) = <instance>.login(user, password)
472 NB: 'password' will be quoted.
474 #if not 'AUTH=LOGIN' in self.capabilities:
475 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
476 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
478 raise self
.error(dat
[-1])
484 """Shutdown connection to server.
486 (typ, [data]) = <instance>.logout()
488 Returns server 'BYE' response.
490 self
.state
= 'LOGOUT'
491 try: typ
, dat
= self
._simple
_command
('LOGOUT')
492 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
494 if 'BYE' in self
.untagged_responses
:
495 return 'BYE', self
.untagged_responses
['BYE']
499 def lsub(self
, directory
='""', pattern
='*'):
500 """List 'subscribed' mailbox names in directory matching pattern.
502 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
504 'data' are tuples of message part envelope and data.
507 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
508 return self
._untagged
_response
(typ
, dat
, name
)
512 """ Returns IMAP namespaces ala rfc2342
514 (typ, [data, ...]) = <instance>.namespace()
517 typ
, dat
= self
._simple
_command
(name
)
518 return self
._untagged
_response
(typ
, dat
, name
)
522 """Send NOOP command.
524 (typ, data) = <instance>.noop()
528 self
._dump
_ur
(self
.untagged_responses
)
529 return self
._simple
_command
('NOOP')
532 def partial(self
, message_num
, message_part
, start
, length
):
533 """Fetch truncated part of a message.
535 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
537 'data' is tuple of message part envelope and data.
540 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
541 return self
._untagged
_response
(typ
, dat
, 'FETCH')
544 def rename(self
, oldmailbox
, newmailbox
):
545 """Rename old mailbox name to new.
547 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
549 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
552 def search(self
, charset
, *criteria
):
553 """Search mailbox for matching messages.
555 (typ, [data]) = <instance>.search(charset, criterium, ...)
557 'data' is space separated list of matching message numbers.
561 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
563 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
564 return self
._untagged
_response
(typ
, dat
, name
)
567 def select(self
, mailbox
='INBOX', readonly
=None):
570 Flush all untagged responses.
572 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
574 'data' is count of messages in mailbox ('EXISTS' response).
576 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
577 self
.untagged_responses
= {} # Flush old responses.
578 self
.is_readonly
= readonly
580 typ
, dat
= self
._simple
_command
(name
, mailbox
)
582 self
.state
= 'AUTH' # Might have been 'SELECTED'
584 self
.state
= 'SELECTED'
585 if 'READ-ONLY' in self
.untagged_responses \
589 self
._dump
_ur
(self
.untagged_responses
)
590 raise self
.readonly('%s is not writable' % mailbox
)
591 return typ
, self
.untagged_responses
.get('EXISTS', [None])
594 def setacl(self
, mailbox
, who
, what
):
595 """Set a mailbox acl.
597 (typ, [data]) = <instance>.create(mailbox, who, what)
599 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
602 def setquota(self
, root
, limits
):
603 """Set the quota root's resource limits.
605 (typ, [data]) = <instance>.setquota(root, limits)
607 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
608 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
611 def sort(self
, sort_criteria
, charset
, *search_criteria
):
612 """IMAP4rev1 extension SORT command.
614 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
617 #if not name in self.capabilities: # Let the server decide!
618 # raise self.error('unimplemented extension command: %s' % name)
619 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
620 sort_criteria
= '(%s)' % sort_criteria
621 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
622 return self
._untagged
_response
(typ
, dat
, name
)
625 def status(self
, mailbox
, names
):
626 """Request named status conditions for mailbox.
628 (typ, [data]) = <instance>.status(mailbox, names)
631 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
632 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
633 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
634 return self
._untagged
_response
(typ
, dat
, name
)
637 def store(self
, message_set
, command
, flags
):
638 """Alters flag dispositions for messages in mailbox.
640 (typ, [data]) = <instance>.store(message_set, command, flags)
642 if (flags
[0],flags
[-1]) != ('(',')'):
643 flags
= '(%s)' % flags
# Avoid quoting the flags
644 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
645 return self
._untagged
_response
(typ
, dat
, 'FETCH')
648 def subscribe(self
, mailbox
):
649 """Subscribe to new mailbox.
651 (typ, [data]) = <instance>.subscribe(mailbox)
653 return self
._simple
_command
('SUBSCRIBE', mailbox
)
656 def uid(self
, command
, *args
):
657 """Execute "command arg ..." with messages identified by UID,
658 rather than message number.
660 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
662 Returns response appropriate to 'command'.
664 command
= command
.upper()
665 if not command
in Commands
:
666 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
667 if self
.state
not in Commands
[command
]:
668 raise self
.error('command %s illegal in state %s'
669 % (command
, self
.state
))
671 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
672 if command
in ('SEARCH', 'SORT'):
676 return self
._untagged
_response
(typ
, dat
, name
)
679 def unsubscribe(self
, mailbox
):
680 """Unsubscribe from old mailbox.
682 (typ, [data]) = <instance>.unsubscribe(mailbox)
684 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
687 def xatom(self
, name
, *args
):
688 """Allow simple extension commands
689 notified by server in CAPABILITY response.
691 Assumes command is legal in current state.
693 (typ, [data]) = <instance>.xatom(name, arg, ...)
695 Returns response appropriate to extension command `name'.
698 #if not name in self.capabilities: # Let the server decide!
699 # raise self.error('unknown extension command: %s' % name)
700 if not name
in Commands
:
701 Commands
[name
] = (self
.state
,)
702 return apply(self
._simple
_command
, (name
,) + args
)
709 def _append_untagged(self
, typ
, dat
):
711 if dat
is None: dat
= ''
712 ur
= self
.untagged_responses
715 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
716 (typ
, len(ur
.get(typ
,'')), dat
))
723 def _check_bye(self
):
724 bye
= self
.untagged_responses
.get('BYE')
726 raise self
.abort(bye
[-1])
729 def _command(self
, name
, *args
):
731 if self
.state
not in Commands
[name
]:
734 'command %s illegal in state %s' % (name
, self
.state
))
736 for typ
in ('OK', 'NO', 'BAD'):
737 if typ
in self
.untagged_responses
:
738 del self
.untagged_responses
[typ
]
740 if 'READ-ONLY' in self
.untagged_responses \
741 and not self
.is_readonly
:
742 raise self
.readonly('mailbox status changed to READ-ONLY')
744 tag
= self
._new
_tag
()
745 data
= '%s %s' % (tag
, name
)
747 if arg
is None: continue
748 data
= '%s %s' % (data
, self
._checkquote
(arg
))
750 literal
= self
.literal
751 if literal
is not None:
753 if type(literal
) is type(self
._command
):
757 data
= '%s {%s}' % (data
, len(literal
))
761 self
._mesg
('> %s' % data
)
763 self
._log
('> %s' % data
)
766 self
.send('%s%s' % (data
, CRLF
))
767 except (socket
.error
, OSError), val
:
768 raise self
.abort('socket error: %s' % val
)
774 # Wait for continuation response
776 while self
._get
_response
():
777 if self
.tagged_commands
[tag
]: # BAD/NO?
783 literal
= literator(self
.continuation_response
)
787 self
._mesg
('write literal size %s' % len(literal
))
792 except (socket
.error
, OSError), val
:
793 raise self
.abort('socket error: %s' % val
)
801 def _command_complete(self
, name
, tag
):
804 typ
, data
= self
._get
_tagged
_response
(tag
)
805 except self
.abort
, val
:
806 raise self
.abort('command: %s => %s' % (name
, val
))
807 except self
.error
, val
:
808 raise self
.error('command: %s => %s' % (name
, val
))
811 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
815 def _get_response(self
):
817 # Read response and store.
819 # Returns None for continuation responses,
820 # otherwise first response line received.
822 resp
= self
._get
_line
()
824 # Command completion response?
826 if self
._match
(self
.tagre
, resp
):
827 tag
= self
.mo
.group('tag')
828 if not tag
in self
.tagged_commands
:
829 raise self
.abort('unexpected tagged response: %s' % resp
)
831 typ
= self
.mo
.group('type')
832 dat
= self
.mo
.group('data')
833 self
.tagged_commands
[tag
] = (typ
, [dat
])
837 # '*' (untagged) responses?
839 if not self
._match
(Untagged_response
, resp
):
840 if self
._match
(Untagged_status
, resp
):
841 dat2
= self
.mo
.group('data2')
844 # Only other possibility is '+' (continuation) response...
846 if self
._match
(Continuation
, resp
):
847 self
.continuation_response
= self
.mo
.group('data')
848 return None # NB: indicates continuation
850 raise self
.abort("unexpected response: '%s'" % resp
)
852 typ
= self
.mo
.group('type')
853 dat
= self
.mo
.group('data')
854 if dat
is None: dat
= '' # Null untagged response
855 if dat2
: dat
= dat
+ ' ' + dat2
857 # Is there a literal to come?
859 while self
._match
(Literal
, dat
):
861 # Read literal direct from connection.
863 size
= int(self
.mo
.group('size'))
866 self
._mesg
('read literal size %s' % size
)
867 data
= self
.read(size
)
869 # Store response with literal as tuple
871 self
._append
_untagged
(typ
, (dat
, data
))
873 # Read trailer - possibly containing another literal
875 dat
= self
._get
_line
()
877 self
._append
_untagged
(typ
, dat
)
879 # Bracketed response information?
881 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
882 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
885 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
886 self
._mesg
('%s response: %s' % (typ
, dat
))
891 def _get_tagged_response(self
, tag
):
894 result
= self
.tagged_commands
[tag
]
895 if result
is not None:
896 del self
.tagged_commands
[tag
]
899 # Some have reported "unexpected response" exceptions.
900 # Note that ignoring them here causes loops.
901 # Instead, send me details of the unexpected response and
902 # I'll update the code in `_get_response()'.
906 except self
.abort
, val
:
915 line
= self
.readline()
917 raise self
.abort('socket error: EOF')
919 # Protocol mandates all lines terminated by CRLF
924 self
._mesg
('< %s' % line
)
926 self
._log
('< %s' % line
)
930 def _match(self
, cre
, s
):
932 # Run compiled regular expression match method on 's'.
933 # Save result, return success.
935 self
.mo
= cre
.match(s
)
937 if self
.mo
is not None and self
.debug
>= 5:
938 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
939 return self
.mo
is not None
944 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
945 self
.tagnum
= self
.tagnum
+ 1
946 self
.tagged_commands
[tag
] = None
950 def _checkquote(self
, arg
):
952 # Must quote command args if non-alphanumeric chars present,
953 # and not already quoted.
955 if type(arg
) is not type(''):
957 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
959 if self
.mustquote
.search(arg
) is None:
961 return self
._quote
(arg
)
964 def _quote(self
, arg
):
966 arg
= arg
.replace('\\', '\\\\')
967 arg
= arg
.replace('"', '\\"')
972 def _simple_command(self
, name
, *args
):
974 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
977 def _untagged_response(self
, typ
, dat
, name
):
981 if not name
in self
.untagged_responses
:
983 data
= self
.untagged_responses
[name
]
986 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
987 del self
.untagged_responses
[name
]
993 def _mesg(self
, s
, secs
=None):
996 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
997 UIBase
.getglobalui().debug('imap', ' %s.%02d %s' % (tm
, (secs
*100)%100, s
))
999 def _dump_ur(self
, dict):
1000 # Dump untagged responses (in `dict').
1004 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1005 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1007 def _log(self
, line
):
1008 # Keep log of last `_cmd_log_len' interactions for debugging.
1009 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1010 self
._cmd
_log
_idx
+= 1
1011 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1012 self
._cmd
_log
_idx
= 0
1014 def print_log(self
):
1015 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1016 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1019 apply(self
._mesg
, self
._cmd
_log
[i
])
1023 if i
>= self
._cmd
_log
_len
:
1027 class IMAP4_Tunnel(IMAP4
):
1028 """IMAP4 client class over a tunnel
1030 Instantiate with: IMAP4_Tunnel(tunnelcmd)
1032 tunnelcmd -- shell command to generate the tunnel.
1033 The result will be in PREAUTH stage."""
1035 def __init__(self
, tunnelcmd
):
1036 IMAP4
.__init__(self
, tunnelcmd
)
1038 def open(self
, host
, port
):
1039 """The tunnelcmd comes in on host!"""
1040 self
.outfd
, self
.infd
= os
.popen2(host
, "t", 0)
1042 def read(self
, size
):
1044 while len(retval
) < size
:
1045 retval
+= self
.infd
.read(size
- len(retval
))
1049 return self
.infd
.readline()
1051 def send(self
, data
):
1052 self
.outfd
.write(data
)
1060 def __init__(self
, sslsock
):
1061 self
.sslsock
= sslsock
1065 return self
.sslsock
.write(s
)
1068 return self
.sslsock
.read(n
)
1071 if len(self
.readbuf
):
1072 # Return the stuff in readbuf, even if less than n.
1073 # It might contain the rest of the line, and if we try to
1074 # read more, might block waiting for data that is not
1076 bytesfrombuf
= min(n
, len(self
.readbuf
))
1077 retval
= self
.readbuf
[:bytesfrombuf
]
1078 self
.readbuf
= self
.readbuf
[bytesfrombuf
:]
1080 retval
= self
._read
(n
)
1082 self
.readbuf
= retval
[n
:]
1089 linebuf
= self
.read(1024)
1090 nlindex
= linebuf
.find("\n")
1092 retval
+= linebuf
[:nlindex
+ 1]
1093 self
.readbuf
= linebuf
[nlindex
+ 1:] + self
.readbuf
1099 class IMAP4_SSL(IMAP4
):
1101 """IMAP4 client class over SSL connection
1103 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1105 host - host's name (default: localhost);
1106 port - port number (default: standard IMAP4 SSL port).
1107 keyfile - PEM formatted file that contains your private key (default: None);
1108 certfile - PEM formatted certificate chain file (default: None);
1110 for more documentation see the docstring of the parent class IMAP4.
1114 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1115 self
.keyfile
= keyfile
1116 self
.certfile
= certfile
1117 IMAP4
.__init__(self
, host
, port
)
1120 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1121 """Setup connection to remote server on "host:port".
1122 (default: localhost:standard IMAP4 SSL port).
1123 This connection will be used by the routines:
1124 read, readline, send, shutdown.
1128 #This connects to the first ip found ipv4/ipv6
1129 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
1130 #example from the python documentation:
1131 #http://www.python.org/doc/lib/socket-example.html
1132 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
1134 af
, socktype
, proto
, canonname
, sa
= res
[0]
1135 self
.sock
= socket
.socket(af
, socktype
, proto
)
1136 self
.sock
.connect(sa
)
1137 if sys
.version_info
[0] <= 2 and sys
.version_info
[1] <= 2:
1138 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1140 self
.sslobj
= socket
.ssl(self
.sock
._sock
, self
.keyfile
, self
.certfile
)
1141 self
.sslobj
= sslwrapper(self
.sslobj
)
1144 def read(self
, size
):
1145 """Read 'size' bytes from remote."""
1147 while len(retval
) < size
:
1148 retval
+= self
.sslobj
.read(size
- len(retval
))
1153 """Read line from remote."""
1154 return self
.sslobj
.readline()
1156 def send(self
, data
):
1157 """Send data to remote."""
1159 bytestowrite
= len(data
)
1160 while byteswritten
< bytestowrite
:
1161 byteswritten
+= self
.sslobj
.write(data
[byteswritten
:])
1165 """Close I/O established in "open"."""
1170 """Return socket instance used to connect to IMAP4 server.
1172 socket = <instance>.socket()
1178 """Return SSLObject instance used to communicate with the IMAP4 server.
1180 ssl = <instance>.socket.ssl()
1186 class _Authenticator
:
1188 """Private class to provide en/decoding
1189 for base64-based authentication conversation.
1192 def __init__(self
, mechinst
):
1193 self
.mech
= mechinst
# Callable object to provide/process data
1195 def process(self
, data
):
1196 ret
= self
.mech(self
.decode(data
))
1198 return '*' # Abort conversation
1199 return self
.encode(ret
)
1201 def encode(self
, inp
):
1203 # Invoke binascii.b2a_base64 iteratively with
1204 # short even length buffers, strip the trailing
1205 # line feed from the result and append. "Even"
1206 # means a number that factors to both 6 and 8,
1207 # so when it gets to the end of the 8-bit input
1208 # there's no partial 6-bit output.
1218 e
= binascii
.b2a_base64(t
)
1223 def decode(self
, inp
):
1226 return binascii
.a2b_base64(inp
)
1230 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1231 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1233 def Internaldate2tuple(resp
):
1234 """Convert IMAP4 INTERNALDATE to UT.
1236 Returns Python time module tuple.
1239 mo
= InternalDate
.match(resp
)
1243 mon
= Mon2num
[mo
.group('mon')]
1244 zonen
= mo
.group('zonen')
1246 day
= int(mo
.group('day'))
1247 year
= int(mo
.group('year'))
1248 hour
= int(mo
.group('hour'))
1249 min = int(mo
.group('min'))
1250 sec
= int(mo
.group('sec'))
1251 zoneh
= int(mo
.group('zoneh'))
1252 zonem
= int(mo
.group('zonem'))
1254 # INTERNALDATE timezone must be subtracted to get UT
1256 zone
= (zoneh
*60 + zonem
)*60
1260 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1262 utc
= time
.mktime(tt
)
1264 # Following is necessary because the time module has no 'mkgmtime'.
1265 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1267 lt
= time
.localtime(utc
)
1268 if time
.daylight
and lt
[-1]:
1269 zone
= zone
+ time
.altzone
1271 zone
= zone
+ time
.timezone
1273 return time
.localtime(utc
- zone
)
1279 """Convert integer to A-P string representation."""
1281 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1284 num
, mod
= divmod(num
, 16)
1290 def ParseFlags(resp
):
1292 """Convert IMAP4 flags response to python tuple."""
1294 mo
= Flags
.match(resp
)
1298 return tuple(mo
.group('flags').split())
1301 def Time2Internaldate(date_time
):
1303 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1305 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1308 if isinstance(date_time
, (int, float)):
1309 tt
= time
.localtime(date_time
)
1310 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1312 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1313 return date_time
# Assume in correct format
1315 raise ValueError("date_time not of a known type")
1317 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1320 if time
.daylight
and tt
[-1]:
1321 zone
= -time
.altzone
1323 zone
= -time
.timezone
1324 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1328 if __name__
== '__main__':
1330 import getopt
, getpass
1333 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1334 except getopt
.error
, val
:
1337 for opt
,val
in optlist
:
1341 if not args
: args
= ('',)
1345 USER
= getpass
.getuser()
1346 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1348 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1350 ('login', (USER
, PASSWD
)),
1351 ('create', ('/tmp/xxx 1',)),
1352 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1353 ('CREATE', ('/tmp/yyz 2',)),
1354 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1355 ('list', ('/tmp', 'yy*')),
1356 ('select', ('/tmp/yyz 2',)),
1357 ('search', (None, 'SUBJECT', 'test')),
1358 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1359 ('store', ('1', 'FLAGS', '(\Deleted)')),
1368 ('response',('UIDVALIDITY',)),
1369 ('uid', ('SEARCH', 'ALL')),
1370 ('response', ('EXISTS',)),
1371 ('append', (None, None, None, test_mesg
)),
1377 M
._mesg
('%s %s' % (cmd
, args
))
1378 typ
, dat
= apply(getattr(M
, cmd
), args
)
1379 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1384 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1385 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1387 for cmd
,args
in test_seq1
:
1390 for ml
in run('list', ('/tmp/', 'yy%')):
1391 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1392 if mo
: path
= mo
.group(1)
1393 else: path
= ml
.split()[-1]
1394 run('delete', (path
,))
1396 for cmd
,args
in test_seq2
:
1397 dat
= run(cmd
, args
)
1399 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1402 uid
= dat
[-1].split()
1403 if not uid
: continue
1404 run('uid', ('FETCH', '%s' % uid
[-1],
1405 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1407 print '\nAll tests OK.'
1410 print '\nTests failed.'
1414 If you would like to see debugging output,