]>
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
)
240 raise socket
.error(last_error
)
241 self
.file = self
.sock
.makefile('rb')
243 def read(self
, size
):
244 """Read 'size' bytes from remote."""
246 while len(retval
) < size
:
247 retval
+= self
.file.read(size
- len(retval
))
251 """Read line from remote."""
252 return self
.file.readline()
255 def send(self
, data
):
256 """Send data to remote."""
257 self
.sock
.sendall(data
)
261 """Close I/O established in "open"."""
267 """Return socket instance used to connect to IMAP4 server.
269 socket = <instance>.socket()
279 """Return most recent 'RECENT' responses if any exist,
280 else prompt server for an update using the 'NOOP' command.
282 (typ, [data]) = <instance>.recent()
284 'data' is None if no new messages,
285 else list of RECENT responses, most recent last.
288 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
291 typ
, dat
= self
.noop() # Prod server for response
292 return self
._untagged
_response
(typ
, dat
, name
)
295 def response(self
, code
):
296 """Return data for response 'code' if received, or None.
298 Old value for response 'code' is cleared.
300 (code, [data]) = <instance>.response(code)
302 return self
._untagged
_response
(code
, [None], code
.upper())
309 def append(self
, mailbox
, flags
, date_time
, message
):
310 """Append message to named mailbox.
312 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
314 All args except `message' can be None.
320 if (flags
[0],flags
[-1]) != ('(',')'):
321 flags
= '(%s)' % flags
325 date_time
= Time2Internaldate(date_time
)
328 self
.literal
= message
329 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
332 def authenticate(self
, mechanism
, authobject
):
333 """Authenticate command - requires response processing.
335 'mechanism' specifies which authentication mechanism is to
336 be used - it must appear in <instance>.capabilities in the
337 form AUTH=<mechanism>.
339 'authobject' must be a callable object:
341 data = authobject(response)
343 It will be called to process server continuation responses.
344 It should return data that will be encoded and sent to server.
345 It should return None if the client abort response '*' should
348 mech
= mechanism
.upper()
349 cap
= 'AUTH=%s' % mech
350 if not cap
in self
.capabilities
:
351 raise self
.error("Server doesn't allow %s authentication." % mech
)
352 self
.literal
= _Authenticator(authobject
).process
353 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
355 raise self
.error(dat
[-1])
361 """Checkpoint mailbox on server.
363 (typ, [data]) = <instance>.check()
365 return self
._simple
_command
('CHECK')
369 """Close currently selected mailbox.
371 Deleted messages are removed from writable mailbox.
372 This is the recommended command before 'LOGOUT'.
374 (typ, [data]) = <instance>.close()
377 typ
, dat
= self
._simple
_command
('CLOSE')
383 def copy(self
, message_set
, new_mailbox
):
384 """Copy 'message_set' messages onto end of 'new_mailbox'.
386 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
388 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
391 def create(self
, mailbox
):
392 """Create new mailbox.
394 (typ, [data]) = <instance>.create(mailbox)
396 return self
._simple
_command
('CREATE', mailbox
)
399 def delete(self
, mailbox
):
400 """Delete old mailbox.
402 (typ, [data]) = <instance>.delete(mailbox)
404 return self
._simple
_command
('DELETE', mailbox
)
408 """Permanently remove deleted items from selected mailbox.
410 Generates 'EXPUNGE' response for each deleted message.
412 (typ, [data]) = <instance>.expunge()
414 'data' is list of 'EXPUNGE'd message numbers in order received.
417 typ
, dat
= self
._simple
_command
(name
)
418 return self
._untagged
_response
(typ
, dat
, name
)
421 def fetch(self
, message_set
, message_parts
):
422 """Fetch (parts of) messages.
424 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
426 'message_parts' should be a string of selected parts
427 enclosed in parentheses, eg: "(UID BODY[TEXT])".
429 'data' are tuples of message part envelope and data.
432 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
433 return self
._untagged
_response
(typ
, dat
, name
)
436 def getacl(self
, mailbox
):
437 """Get the ACLs for a mailbox.
439 (typ, [data]) = <instance>.getacl(mailbox)
441 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
442 return self
._untagged
_response
(typ
, dat
, 'ACL')
445 def getquota(self
, root
):
446 """Get the quota root's resource usage and limits.
448 Part of the IMAP4 QUOTA extension defined in rfc2087.
450 (typ, [data]) = <instance>.getquota(root)
452 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
453 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
456 def getquotaroot(self
, mailbox
):
457 """Get the list of quota roots for the named mailbox.
459 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
461 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
462 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
463 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
464 return typ
, [quotaroot
, quota
]
467 def list(self
, directory
='""', pattern
='*'):
468 """List mailbox names in directory matching pattern.
470 (typ, [data]) = <instance>.list(directory='""', pattern='*')
472 'data' is list of LIST responses.
475 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
476 return self
._untagged
_response
(typ
, dat
, name
)
479 def login(self
, user
, password
):
480 """Identify client using plaintext password.
482 (typ, [data]) = <instance>.login(user, password)
484 NB: 'password' will be quoted.
486 #if not 'AUTH=LOGIN' in self.capabilities:
487 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
488 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
490 raise self
.error(dat
[-1])
496 """Shutdown connection to server.
498 (typ, [data]) = <instance>.logout()
500 Returns server 'BYE' response.
502 self
.state
= 'LOGOUT'
503 try: typ
, dat
= self
._simple
_command
('LOGOUT')
504 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
506 if 'BYE' in self
.untagged_responses
:
507 return 'BYE', self
.untagged_responses
['BYE']
511 def lsub(self
, directory
='""', pattern
='*'):
512 """List 'subscribed' mailbox names in directory matching pattern.
514 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
516 'data' are tuples of message part envelope and data.
519 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
520 return self
._untagged
_response
(typ
, dat
, name
)
524 """ Returns IMAP namespaces ala rfc2342
526 (typ, [data, ...]) = <instance>.namespace()
529 typ
, dat
= self
._simple
_command
(name
)
530 return self
._untagged
_response
(typ
, dat
, name
)
534 """Send NOOP command.
536 (typ, data) = <instance>.noop()
540 self
._dump
_ur
(self
.untagged_responses
)
541 return self
._simple
_command
('NOOP')
544 def partial(self
, message_num
, message_part
, start
, length
):
545 """Fetch truncated part of a message.
547 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
549 'data' is tuple of message part envelope and data.
552 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
553 return self
._untagged
_response
(typ
, dat
, 'FETCH')
556 def rename(self
, oldmailbox
, newmailbox
):
557 """Rename old mailbox name to new.
559 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
561 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
564 def search(self
, charset
, *criteria
):
565 """Search mailbox for matching messages.
567 (typ, [data]) = <instance>.search(charset, criterium, ...)
569 'data' is space separated list of matching message numbers.
573 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
575 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
576 return self
._untagged
_response
(typ
, dat
, name
)
579 def select(self
, mailbox
='INBOX', readonly
=None):
582 Flush all untagged responses.
584 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
586 'data' is count of messages in mailbox ('EXISTS' response).
588 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
589 self
.untagged_responses
= {} # Flush old responses.
590 self
.is_readonly
= readonly
592 typ
, dat
= self
._simple
_command
(name
, mailbox
)
594 self
.state
= 'AUTH' # Might have been 'SELECTED'
596 self
.state
= 'SELECTED'
597 if 'READ-ONLY' in self
.untagged_responses \
601 self
._dump
_ur
(self
.untagged_responses
)
602 raise self
.readonly('%s is not writable' % mailbox
)
603 return typ
, self
.untagged_responses
.get('EXISTS', [None])
606 def setacl(self
, mailbox
, who
, what
):
607 """Set a mailbox acl.
609 (typ, [data]) = <instance>.create(mailbox, who, what)
611 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
614 def setquota(self
, root
, limits
):
615 """Set the quota root's resource limits.
617 (typ, [data]) = <instance>.setquota(root, limits)
619 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
620 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
623 def sort(self
, sort_criteria
, charset
, *search_criteria
):
624 """IMAP4rev1 extension SORT command.
626 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
629 #if not name in self.capabilities: # Let the server decide!
630 # raise self.error('unimplemented extension command: %s' % name)
631 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
632 sort_criteria
= '(%s)' % sort_criteria
633 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
634 return self
._untagged
_response
(typ
, dat
, name
)
637 def status(self
, mailbox
, names
):
638 """Request named status conditions for mailbox.
640 (typ, [data]) = <instance>.status(mailbox, names)
643 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
644 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
645 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
646 return self
._untagged
_response
(typ
, dat
, name
)
649 def store(self
, message_set
, command
, flags
):
650 """Alters flag dispositions for messages in mailbox.
652 (typ, [data]) = <instance>.store(message_set, command, flags)
654 if (flags
[0],flags
[-1]) != ('(',')'):
655 flags
= '(%s)' % flags
# Avoid quoting the flags
656 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
657 return self
._untagged
_response
(typ
, dat
, 'FETCH')
660 def subscribe(self
, mailbox
):
661 """Subscribe to new mailbox.
663 (typ, [data]) = <instance>.subscribe(mailbox)
665 return self
._simple
_command
('SUBSCRIBE', mailbox
)
668 def uid(self
, command
, *args
):
669 """Execute "command arg ..." with messages identified by UID,
670 rather than message number.
672 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
674 Returns response appropriate to 'command'.
676 command
= command
.upper()
677 if not command
in Commands
:
678 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
679 if self
.state
not in Commands
[command
]:
680 raise self
.error('command %s illegal in state %s'
681 % (command
, self
.state
))
683 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
684 if command
in ('SEARCH', 'SORT'):
688 return self
._untagged
_response
(typ
, dat
, name
)
691 def unsubscribe(self
, mailbox
):
692 """Unsubscribe from old mailbox.
694 (typ, [data]) = <instance>.unsubscribe(mailbox)
696 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
699 def xatom(self
, name
, *args
):
700 """Allow simple extension commands
701 notified by server in CAPABILITY response.
703 Assumes command is legal in current state.
705 (typ, [data]) = <instance>.xatom(name, arg, ...)
707 Returns response appropriate to extension command `name'.
710 #if not name in self.capabilities: # Let the server decide!
711 # raise self.error('unknown extension command: %s' % name)
712 if not name
in Commands
:
713 Commands
[name
] = (self
.state
,)
714 return apply(self
._simple
_command
, (name
,) + args
)
721 def _append_untagged(self
, typ
, dat
):
723 if dat
is None: dat
= ''
724 ur
= self
.untagged_responses
727 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
728 (typ
, len(ur
.get(typ
,'')), dat
))
735 def _check_bye(self
):
736 bye
= self
.untagged_responses
.get('BYE')
738 raise self
.abort(bye
[-1])
741 def _command(self
, name
, *args
):
743 if self
.state
not in Commands
[name
]:
746 'command %s illegal in state %s' % (name
, self
.state
))
748 for typ
in ('OK', 'NO', 'BAD'):
749 if typ
in self
.untagged_responses
:
750 del self
.untagged_responses
[typ
]
752 if 'READ-ONLY' in self
.untagged_responses \
753 and not self
.is_readonly
:
754 raise self
.readonly('mailbox status changed to READ-ONLY')
756 tag
= self
._new
_tag
()
757 data
= '%s %s' % (tag
, name
)
759 if arg
is None: continue
760 data
= '%s %s' % (data
, self
._checkquote
(arg
))
762 literal
= self
.literal
763 if literal
is not None:
765 if type(literal
) is type(self
._command
):
769 data
= '%s {%s}' % (data
, len(literal
))
773 self
._mesg
('> %s' % data
)
775 self
._log
('> %s' % data
)
778 self
.send('%s%s' % (data
, CRLF
))
779 except (socket
.error
, OSError), val
:
780 raise self
.abort('socket error: %s' % val
)
786 # Wait for continuation response
788 while self
._get
_response
():
789 if self
.tagged_commands
[tag
]: # BAD/NO?
795 literal
= literator(self
.continuation_response
)
799 self
._mesg
('write literal size %s' % len(literal
))
804 except (socket
.error
, OSError), val
:
805 raise self
.abort('socket error: %s' % val
)
813 def _command_complete(self
, name
, tag
):
816 typ
, data
= self
._get
_tagged
_response
(tag
)
817 except self
.abort
, val
:
818 raise self
.abort('command: %s => %s' % (name
, val
))
819 except self
.error
, val
:
820 raise self
.error('command: %s => %s' % (name
, val
))
823 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
827 def _get_response(self
):
829 # Read response and store.
831 # Returns None for continuation responses,
832 # otherwise first response line received.
834 resp
= self
._get
_line
()
836 # Command completion response?
838 if self
._match
(self
.tagre
, resp
):
839 tag
= self
.mo
.group('tag')
840 if not tag
in self
.tagged_commands
:
841 raise self
.abort('unexpected tagged response: %s' % resp
)
843 typ
= self
.mo
.group('type')
844 dat
= self
.mo
.group('data')
845 self
.tagged_commands
[tag
] = (typ
, [dat
])
849 # '*' (untagged) responses?
851 if not self
._match
(Untagged_response
, resp
):
852 if self
._match
(Untagged_status
, resp
):
853 dat2
= self
.mo
.group('data2')
856 # Only other possibility is '+' (continuation) response...
858 if self
._match
(Continuation
, resp
):
859 self
.continuation_response
= self
.mo
.group('data')
860 return None # NB: indicates continuation
862 raise self
.abort("unexpected response: '%s'" % resp
)
864 typ
= self
.mo
.group('type')
865 dat
= self
.mo
.group('data')
866 if dat
is None: dat
= '' # Null untagged response
867 if dat2
: dat
= dat
+ ' ' + dat2
869 # Is there a literal to come?
871 while self
._match
(Literal
, dat
):
873 # Read literal direct from connection.
875 size
= int(self
.mo
.group('size'))
878 self
._mesg
('read literal size %s' % size
)
879 data
= self
.read(size
)
881 # Store response with literal as tuple
883 self
._append
_untagged
(typ
, (dat
, data
))
885 # Read trailer - possibly containing another literal
887 dat
= self
._get
_line
()
889 self
._append
_untagged
(typ
, dat
)
891 # Bracketed response information?
893 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
894 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
897 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
898 self
._mesg
('%s response: %s' % (typ
, dat
))
903 def _get_tagged_response(self
, tag
):
906 result
= self
.tagged_commands
[tag
]
907 if result
is not None:
908 del self
.tagged_commands
[tag
]
911 # Some have reported "unexpected response" exceptions.
912 # Note that ignoring them here causes loops.
913 # Instead, send me details of the unexpected response and
914 # I'll update the code in `_get_response()'.
918 except self
.abort
, val
:
927 line
= self
.readline()
929 raise self
.abort('socket error: EOF')
931 # Protocol mandates all lines terminated by CRLF
936 self
._mesg
('< %s' % line
)
938 self
._log
('< %s' % line
)
942 def _match(self
, cre
, s
):
944 # Run compiled regular expression match method on 's'.
945 # Save result, return success.
947 self
.mo
= cre
.match(s
)
949 if self
.mo
is not None and self
.debug
>= 5:
950 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
951 return self
.mo
is not None
956 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
957 self
.tagnum
= self
.tagnum
+ 1
958 self
.tagged_commands
[tag
] = None
962 def _checkquote(self
, arg
):
964 # Must quote command args if non-alphanumeric chars present,
965 # and not already quoted.
967 if type(arg
) is not type(''):
969 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
971 if self
.mustquote
.search(arg
) is None:
973 return self
._quote
(arg
)
976 def _quote(self
, arg
):
978 arg
= arg
.replace('\\', '\\\\')
979 arg
= arg
.replace('"', '\\"')
984 def _simple_command(self
, name
, *args
):
986 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
989 def _untagged_response(self
, typ
, dat
, name
):
993 if not name
in self
.untagged_responses
:
995 data
= self
.untagged_responses
[name
]
998 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
999 del self
.untagged_responses
[name
]
1005 def _mesg(self
, s
, secs
=None):
1008 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1009 UIBase
.getglobalui().debug('imap', ' %s.%02d %s' % (tm
, (secs
*100)%100, s
))
1011 def _dump_ur(self
, dict):
1012 # Dump untagged responses (in `dict').
1016 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1017 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1019 def _log(self
, line
):
1020 # Keep log of last `_cmd_log_len' interactions for debugging.
1021 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1022 self
._cmd
_log
_idx
+= 1
1023 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1024 self
._cmd
_log
_idx
= 0
1026 def print_log(self
):
1027 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1028 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1031 apply(self
._mesg
, self
._cmd
_log
[i
])
1035 if i
>= self
._cmd
_log
_len
:
1039 class IMAP4_Tunnel(IMAP4
):
1040 """IMAP4 client class over a tunnel
1042 Instantiate with: IMAP4_Tunnel(tunnelcmd)
1044 tunnelcmd -- shell command to generate the tunnel.
1045 The result will be in PREAUTH stage."""
1047 def __init__(self
, tunnelcmd
):
1048 IMAP4
.__init__(self
, tunnelcmd
)
1050 def open(self
, host
, port
):
1051 """The tunnelcmd comes in on host!"""
1052 self
.process
= subprocess
.Popen(host
, shell
=True, close_fds
=True,
1053 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
)
1054 (self
.outfd
, self
.infd
) = (self
.process
.stdin
, self
.process
.stdout
)
1056 def read(self
, size
):
1058 while len(retval
) < size
:
1059 retval
+= self
.infd
.read(size
- len(retval
))
1063 return self
.infd
.readline()
1065 def send(self
, data
):
1066 self
.outfd
.write(data
)
1075 def __init__(self
, sslsock
):
1076 self
.sslsock
= sslsock
1080 return self
.sslsock
.write(s
)
1083 return self
.sslsock
.read(n
)
1086 if len(self
.readbuf
):
1087 # Return the stuff in readbuf, even if less than n.
1088 # It might contain the rest of the line, and if we try to
1089 # read more, might block waiting for data that is not
1091 bytesfrombuf
= min(n
, len(self
.readbuf
))
1092 retval
= self
.readbuf
[:bytesfrombuf
]
1093 self
.readbuf
= self
.readbuf
[bytesfrombuf
:]
1095 retval
= self
._read
(n
)
1097 self
.readbuf
= retval
[n
:]
1104 linebuf
= self
.read(1024)
1105 nlindex
= linebuf
.find("\n")
1107 retval
+= linebuf
[:nlindex
+ 1]
1108 self
.readbuf
= linebuf
[nlindex
+ 1:] + self
.readbuf
1114 class IMAP4_SSL(IMAP4
):
1116 """IMAP4 client class over SSL connection
1118 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1120 host - host's name (default: localhost);
1121 port - port number (default: standard IMAP4 SSL port).
1122 keyfile - PEM formatted file that contains your private key (default: None);
1123 certfile - PEM formatted certificate chain file (default: None);
1125 for more documentation see the docstring of the parent class IMAP4.
1129 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1130 self
.keyfile
= keyfile
1131 self
.certfile
= certfile
1132 IMAP4
.__init__(self
, host
, port
)
1135 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1136 """Setup connection to remote server on "host:port".
1137 (default: localhost:standard IMAP4 SSL port).
1138 This connection will be used by the routines:
1139 read, readline, send, shutdown.
1143 #This connects to the first ip found ipv4/ipv6
1144 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
1145 #example from the python documentation:
1146 #http://www.python.org/doc/lib/socket-example.html
1147 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
1149 # Try all the addresses in turn until we connect()
1152 af
, socktype
, proto
, canonname
, sa
= remote
1153 self
.sock
= socket
.socket(af
, socktype
, proto
)
1154 last_error
= self
.sock
.connect_ex(sa
)
1162 raise socket
.error(last_error
)
1163 if sys
.version_info
[0] <= 2 and sys
.version_info
[1] <= 2:
1164 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1166 self
.sslobj
= socket
.ssl(self
.sock
._sock
, self
.keyfile
, self
.certfile
)
1167 self
.sslobj
= sslwrapper(self
.sslobj
)
1170 def read(self
, size
):
1171 """Read 'size' bytes from remote."""
1173 while len(retval
) < size
:
1174 retval
+= self
.sslobj
.read(size
- len(retval
))
1179 """Read line from remote."""
1180 return self
.sslobj
.readline()
1182 def send(self
, data
):
1183 """Send data to remote."""
1185 bytestowrite
= len(data
)
1186 while byteswritten
< bytestowrite
:
1187 byteswritten
+= self
.sslobj
.write(data
[byteswritten
:])
1191 """Close I/O established in "open"."""
1196 """Return socket instance used to connect to IMAP4 server.
1198 socket = <instance>.socket()
1204 """Return SSLObject instance used to communicate with the IMAP4 server.
1206 ssl = <instance>.socket.ssl()
1212 class _Authenticator
:
1214 """Private class to provide en/decoding
1215 for base64-based authentication conversation.
1218 def __init__(self
, mechinst
):
1219 self
.mech
= mechinst
# Callable object to provide/process data
1221 def process(self
, data
):
1222 ret
= self
.mech(self
.decode(data
))
1224 return '*' # Abort conversation
1225 return self
.encode(ret
)
1227 def encode(self
, inp
):
1229 # Invoke binascii.b2a_base64 iteratively with
1230 # short even length buffers, strip the trailing
1231 # line feed from the result and append. "Even"
1232 # means a number that factors to both 6 and 8,
1233 # so when it gets to the end of the 8-bit input
1234 # there's no partial 6-bit output.
1244 e
= binascii
.b2a_base64(t
)
1249 def decode(self
, inp
):
1252 return binascii
.a2b_base64(inp
)
1256 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1257 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1259 def Internaldate2epoch(resp
):
1260 """Convert IMAP4 INTERNALDATE to UT.
1262 Returns seconds since the epoch.
1265 mo
= InternalDate
.match(resp
)
1269 mon
= Mon2num
[mo
.group('mon')]
1270 zonen
= mo
.group('zonen')
1272 day
= int(mo
.group('day'))
1273 year
= int(mo
.group('year'))
1274 hour
= int(mo
.group('hour'))
1275 min = int(mo
.group('min'))
1276 sec
= int(mo
.group('sec'))
1277 zoneh
= int(mo
.group('zoneh'))
1278 zonem
= int(mo
.group('zonem'))
1280 # INTERNALDATE timezone must be subtracted to get UT
1282 zone
= (zoneh
*60 + zonem
)*60
1286 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1288 return time
.mktime(tt
)
1291 def Internaldate2tuple(resp
):
1292 """Convert IMAP4 INTERNALDATE to UT.
1294 Returns Python time module tuple.
1297 utc
= Internaldate2epoch(resp
)
1299 # Following is necessary because the time module has no 'mkgmtime'.
1300 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1302 lt
= time
.localtime(utc
)
1303 if time
.daylight
and lt
[-1]:
1304 zone
= zone
+ time
.altzone
1306 zone
= zone
+ time
.timezone
1308 return time
.localtime(utc
- zone
)
1314 """Convert integer to A-P string representation."""
1316 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1319 num
, mod
= divmod(num
, 16)
1325 def ParseFlags(resp
):
1327 """Convert IMAP4 flags response to python tuple."""
1329 mo
= Flags
.match(resp
)
1333 return tuple(mo
.group('flags').split())
1336 def Time2Internaldate(date_time
):
1338 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1340 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1343 if isinstance(date_time
, (int, float)):
1344 tt
= time
.localtime(date_time
)
1345 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1347 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1348 return date_time
# Assume in correct format
1350 raise ValueError("date_time not of a known type")
1352 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1355 if time
.daylight
and tt
[-1]:
1356 zone
= -time
.altzone
1358 zone
= -time
.timezone
1359 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1363 if __name__
== '__main__':
1365 import getopt
, getpass
1368 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1369 except getopt
.error
, val
:
1372 for opt
,val
in optlist
:
1376 if not args
: args
= ('',)
1380 USER
= getpass
.getuser()
1381 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1383 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1385 ('login', (USER
, PASSWD
)),
1386 ('create', ('/tmp/xxx 1',)),
1387 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1388 ('CREATE', ('/tmp/yyz 2',)),
1389 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1390 ('list', ('/tmp', 'yy*')),
1391 ('select', ('/tmp/yyz 2',)),
1392 ('search', (None, 'SUBJECT', 'test')),
1393 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1394 ('store', ('1', 'FLAGS', '(\Deleted)')),
1403 ('response',('UIDVALIDITY',)),
1404 ('uid', ('SEARCH', 'ALL')),
1405 ('response', ('EXISTS',)),
1406 ('append', (None, None, None, test_mesg
)),
1412 M
._mesg
('%s %s' % (cmd
, args
))
1413 typ
, dat
= apply(getattr(M
, cmd
), args
)
1414 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1419 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1420 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1422 for cmd
,args
in test_seq1
:
1425 for ml
in run('list', ('/tmp/', 'yy%')):
1426 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1427 if mo
: path
= mo
.group(1)
1428 else: path
= ml
.split()[-1]
1429 run('delete', (path
,))
1431 for cmd
,args
in test_seq2
:
1432 dat
= run(cmd
, args
)
1434 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1437 uid
= dat
[-1].split()
1438 if not uid
: continue
1439 run('uid', ('FETCH', '%s' % uid
[-1],
1440 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1442 print '\nAll tests OK.'
1445 print '\nTests failed.'
1449 If you would like to see debugging output,