]>
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
, 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 #This connects to the first ip found ipv4/ipv6
222 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
223 #example from the python documentation:
224 #http://www.python.org/doc/lib/socket-example.html
225 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
227 af
, socktype
, proto
, canonname
, sa
= res
[0]
228 self
.sock
= socket
.socket(af
, socktype
, proto
)
229 self
.sock
.connect(sa
)
230 self
.file = self
.sock
.makefile('rb')
232 def read(self
, size
):
233 """Read 'size' bytes from remote."""
235 while len(retval
) < size
:
236 retval
+= self
.file.read(size
- len(retval
))
240 """Read line from remote."""
241 return self
.file.readline()
244 def send(self
, data
):
245 """Send data to remote."""
246 self
.sock
.sendall(data
)
250 """Close I/O established in "open"."""
256 """Return socket instance used to connect to IMAP4 server.
258 socket = <instance>.socket()
268 """Return most recent 'RECENT' responses if any exist,
269 else prompt server for an update using the 'NOOP' command.
271 (typ, [data]) = <instance>.recent()
273 'data' is None if no new messages,
274 else list of RECENT responses, most recent last.
277 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
280 typ
, dat
= self
.noop() # Prod server for response
281 return self
._untagged
_response
(typ
, dat
, name
)
284 def response(self
, code
):
285 """Return data for response 'code' if received, or None.
287 Old value for response 'code' is cleared.
289 (code, [data]) = <instance>.response(code)
291 return self
._untagged
_response
(code
, [None], code
.upper())
298 def append(self
, mailbox
, flags
, date_time
, message
):
299 """Append message to named mailbox.
301 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
303 All args except `message' can be None.
309 if (flags
[0],flags
[-1]) != ('(',')'):
310 flags
= '(%s)' % flags
314 date_time
= Time2Internaldate(date_time
)
317 self
.literal
= message
318 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
321 def authenticate(self
, mechanism
, authobject
):
322 """Authenticate command - requires response processing.
324 'mechanism' specifies which authentication mechanism is to
325 be used - it must appear in <instance>.capabilities in the
326 form AUTH=<mechanism>.
328 'authobject' must be a callable object:
330 data = authobject(response)
332 It will be called to process server continuation responses.
333 It should return data that will be encoded and sent to server.
334 It should return None if the client abort response '*' should
337 mech
= mechanism
.upper()
338 cap
= 'AUTH=%s' % mech
339 if not cap
in self
.capabilities
:
340 raise self
.error("Server doesn't allow %s authentication." % mech
)
341 self
.literal
= _Authenticator(authobject
).process
342 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
344 raise self
.error(dat
[-1])
350 """Checkpoint mailbox on server.
352 (typ, [data]) = <instance>.check()
354 return self
._simple
_command
('CHECK')
358 """Close currently selected mailbox.
360 Deleted messages are removed from writable mailbox.
361 This is the recommended command before 'LOGOUT'.
363 (typ, [data]) = <instance>.close()
366 typ
, dat
= self
._simple
_command
('CLOSE')
372 def copy(self
, message_set
, new_mailbox
):
373 """Copy 'message_set' messages onto end of 'new_mailbox'.
375 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
377 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
380 def create(self
, mailbox
):
381 """Create new mailbox.
383 (typ, [data]) = <instance>.create(mailbox)
385 return self
._simple
_command
('CREATE', mailbox
)
388 def delete(self
, mailbox
):
389 """Delete old mailbox.
391 (typ, [data]) = <instance>.delete(mailbox)
393 return self
._simple
_command
('DELETE', mailbox
)
397 """Permanently remove deleted items from selected mailbox.
399 Generates 'EXPUNGE' response for each deleted message.
401 (typ, [data]) = <instance>.expunge()
403 'data' is list of 'EXPUNGE'd message numbers in order received.
406 typ
, dat
= self
._simple
_command
(name
)
407 return self
._untagged
_response
(typ
, dat
, name
)
410 def fetch(self
, message_set
, message_parts
):
411 """Fetch (parts of) messages.
413 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
415 'message_parts' should be a string of selected parts
416 enclosed in parentheses, eg: "(UID BODY[TEXT])".
418 'data' are tuples of message part envelope and data.
421 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
422 return self
._untagged
_response
(typ
, dat
, name
)
425 def getacl(self
, mailbox
):
426 """Get the ACLs for a mailbox.
428 (typ, [data]) = <instance>.getacl(mailbox)
430 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
431 return self
._untagged
_response
(typ
, dat
, 'ACL')
434 def getquota(self
, root
):
435 """Get the quota root's resource usage and limits.
437 Part of the IMAP4 QUOTA extension defined in rfc2087.
439 (typ, [data]) = <instance>.getquota(root)
441 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
442 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
445 def getquotaroot(self
, mailbox
):
446 """Get the list of quota roots for the named mailbox.
448 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
450 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
451 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
452 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
453 return typ
, [quotaroot
, quota
]
456 def list(self
, directory
='""', pattern
='*'):
457 """List mailbox names in directory matching pattern.
459 (typ, [data]) = <instance>.list(directory='""', pattern='*')
461 'data' is list of LIST responses.
464 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
465 return self
._untagged
_response
(typ
, dat
, name
)
468 def login(self
, user
, password
):
469 """Identify client using plaintext password.
471 (typ, [data]) = <instance>.login(user, password)
473 NB: 'password' will be quoted.
475 #if not 'AUTH=LOGIN' in self.capabilities:
476 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
477 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
479 raise self
.error(dat
[-1])
485 """Shutdown connection to server.
487 (typ, [data]) = <instance>.logout()
489 Returns server 'BYE' response.
491 self
.state
= 'LOGOUT'
492 try: typ
, dat
= self
._simple
_command
('LOGOUT')
493 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
495 if 'BYE' in self
.untagged_responses
:
496 return 'BYE', self
.untagged_responses
['BYE']
500 def lsub(self
, directory
='""', pattern
='*'):
501 """List 'subscribed' mailbox names in directory matching pattern.
503 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
505 'data' are tuples of message part envelope and data.
508 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
509 return self
._untagged
_response
(typ
, dat
, name
)
513 """ Returns IMAP namespaces ala rfc2342
515 (typ, [data, ...]) = <instance>.namespace()
518 typ
, dat
= self
._simple
_command
(name
)
519 return self
._untagged
_response
(typ
, dat
, name
)
523 """Send NOOP command.
525 (typ, data) = <instance>.noop()
529 self
._dump
_ur
(self
.untagged_responses
)
530 return self
._simple
_command
('NOOP')
533 def partial(self
, message_num
, message_part
, start
, length
):
534 """Fetch truncated part of a message.
536 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
538 'data' is tuple of message part envelope and data.
541 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
542 return self
._untagged
_response
(typ
, dat
, 'FETCH')
545 def rename(self
, oldmailbox
, newmailbox
):
546 """Rename old mailbox name to new.
548 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
550 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
553 def search(self
, charset
, *criteria
):
554 """Search mailbox for matching messages.
556 (typ, [data]) = <instance>.search(charset, criterium, ...)
558 'data' is space separated list of matching message numbers.
562 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
564 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
565 return self
._untagged
_response
(typ
, dat
, name
)
568 def select(self
, mailbox
='INBOX', readonly
=None):
571 Flush all untagged responses.
573 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
575 'data' is count of messages in mailbox ('EXISTS' response).
577 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
578 self
.untagged_responses
= {} # Flush old responses.
579 self
.is_readonly
= readonly
581 typ
, dat
= self
._simple
_command
(name
, mailbox
)
583 self
.state
= 'AUTH' # Might have been 'SELECTED'
585 self
.state
= 'SELECTED'
586 if 'READ-ONLY' in self
.untagged_responses \
590 self
._dump
_ur
(self
.untagged_responses
)
591 raise self
.readonly('%s is not writable' % mailbox
)
592 return typ
, self
.untagged_responses
.get('EXISTS', [None])
595 def setacl(self
, mailbox
, who
, what
):
596 """Set a mailbox acl.
598 (typ, [data]) = <instance>.create(mailbox, who, what)
600 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
603 def setquota(self
, root
, limits
):
604 """Set the quota root's resource limits.
606 (typ, [data]) = <instance>.setquota(root, limits)
608 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
609 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
612 def sort(self
, sort_criteria
, charset
, *search_criteria
):
613 """IMAP4rev1 extension SORT command.
615 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
618 #if not name in self.capabilities: # Let the server decide!
619 # raise self.error('unimplemented extension command: %s' % name)
620 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
621 sort_criteria
= '(%s)' % sort_criteria
622 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
623 return self
._untagged
_response
(typ
, dat
, name
)
626 def status(self
, mailbox
, names
):
627 """Request named status conditions for mailbox.
629 (typ, [data]) = <instance>.status(mailbox, names)
632 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
633 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
634 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
635 return self
._untagged
_response
(typ
, dat
, name
)
638 def store(self
, message_set
, command
, flags
):
639 """Alters flag dispositions for messages in mailbox.
641 (typ, [data]) = <instance>.store(message_set, command, flags)
643 if (flags
[0],flags
[-1]) != ('(',')'):
644 flags
= '(%s)' % flags
# Avoid quoting the flags
645 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
646 return self
._untagged
_response
(typ
, dat
, 'FETCH')
649 def subscribe(self
, mailbox
):
650 """Subscribe to new mailbox.
652 (typ, [data]) = <instance>.subscribe(mailbox)
654 return self
._simple
_command
('SUBSCRIBE', mailbox
)
657 def uid(self
, command
, *args
):
658 """Execute "command arg ..." with messages identified by UID,
659 rather than message number.
661 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
663 Returns response appropriate to 'command'.
665 command
= command
.upper()
666 if not command
in Commands
:
667 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
668 if self
.state
not in Commands
[command
]:
669 raise self
.error('command %s illegal in state %s'
670 % (command
, self
.state
))
672 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
673 if command
in ('SEARCH', 'SORT'):
677 return self
._untagged
_response
(typ
, dat
, name
)
680 def unsubscribe(self
, mailbox
):
681 """Unsubscribe from old mailbox.
683 (typ, [data]) = <instance>.unsubscribe(mailbox)
685 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
688 def xatom(self
, name
, *args
):
689 """Allow simple extension commands
690 notified by server in CAPABILITY response.
692 Assumes command is legal in current state.
694 (typ, [data]) = <instance>.xatom(name, arg, ...)
696 Returns response appropriate to extension command `name'.
699 #if not name in self.capabilities: # Let the server decide!
700 # raise self.error('unknown extension command: %s' % name)
701 if not name
in Commands
:
702 Commands
[name
] = (self
.state
,)
703 return apply(self
._simple
_command
, (name
,) + args
)
710 def _append_untagged(self
, typ
, dat
):
712 if dat
is None: dat
= ''
713 ur
= self
.untagged_responses
716 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
717 (typ
, len(ur
.get(typ
,'')), dat
))
724 def _check_bye(self
):
725 bye
= self
.untagged_responses
.get('BYE')
727 raise self
.abort(bye
[-1])
730 def _command(self
, name
, *args
):
732 if self
.state
not in Commands
[name
]:
735 'command %s illegal in state %s' % (name
, self
.state
))
737 for typ
in ('OK', 'NO', 'BAD'):
738 if typ
in self
.untagged_responses
:
739 del self
.untagged_responses
[typ
]
741 if 'READ-ONLY' in self
.untagged_responses \
742 and not self
.is_readonly
:
743 raise self
.readonly('mailbox status changed to READ-ONLY')
745 tag
= self
._new
_tag
()
746 data
= '%s %s' % (tag
, name
)
748 if arg
is None: continue
749 data
= '%s %s' % (data
, self
._checkquote
(arg
))
751 literal
= self
.literal
752 if literal
is not None:
754 if type(literal
) is type(self
._command
):
758 data
= '%s {%s}' % (data
, len(literal
))
762 self
._mesg
('> %s' % data
)
764 self
._log
('> %s' % data
)
767 self
.send('%s%s' % (data
, CRLF
))
768 except (socket
.error
, OSError), val
:
769 raise self
.abort('socket error: %s' % val
)
775 # Wait for continuation response
777 while self
._get
_response
():
778 if self
.tagged_commands
[tag
]: # BAD/NO?
784 literal
= literator(self
.continuation_response
)
788 self
._mesg
('write literal size %s' % len(literal
))
793 except (socket
.error
, OSError), val
:
794 raise self
.abort('socket error: %s' % val
)
802 def _command_complete(self
, name
, tag
):
805 typ
, data
= self
._get
_tagged
_response
(tag
)
806 except self
.abort
, val
:
807 raise self
.abort('command: %s => %s' % (name
, val
))
808 except self
.error
, val
:
809 raise self
.error('command: %s => %s' % (name
, val
))
812 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
816 def _get_response(self
):
818 # Read response and store.
820 # Returns None for continuation responses,
821 # otherwise first response line received.
823 resp
= self
._get
_line
()
825 # Command completion response?
827 if self
._match
(self
.tagre
, resp
):
828 tag
= self
.mo
.group('tag')
829 if not tag
in self
.tagged_commands
:
830 raise self
.abort('unexpected tagged response: %s' % resp
)
832 typ
= self
.mo
.group('type')
833 dat
= self
.mo
.group('data')
834 self
.tagged_commands
[tag
] = (typ
, [dat
])
838 # '*' (untagged) responses?
840 if not self
._match
(Untagged_response
, resp
):
841 if self
._match
(Untagged_status
, resp
):
842 dat2
= self
.mo
.group('data2')
845 # Only other possibility is '+' (continuation) response...
847 if self
._match
(Continuation
, resp
):
848 self
.continuation_response
= self
.mo
.group('data')
849 return None # NB: indicates continuation
851 raise self
.abort("unexpected response: '%s'" % resp
)
853 typ
= self
.mo
.group('type')
854 dat
= self
.mo
.group('data')
855 if dat
is None: dat
= '' # Null untagged response
856 if dat2
: dat
= dat
+ ' ' + dat2
858 # Is there a literal to come?
860 while self
._match
(Literal
, dat
):
862 # Read literal direct from connection.
864 size
= int(self
.mo
.group('size'))
867 self
._mesg
('read literal size %s' % size
)
868 data
= self
.read(size
)
870 # Store response with literal as tuple
872 self
._append
_untagged
(typ
, (dat
, data
))
874 # Read trailer - possibly containing another literal
876 dat
= self
._get
_line
()
878 self
._append
_untagged
(typ
, dat
)
880 # Bracketed response information?
882 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
883 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
886 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
887 self
._mesg
('%s response: %s' % (typ
, dat
))
892 def _get_tagged_response(self
, tag
):
895 result
= self
.tagged_commands
[tag
]
896 if result
is not None:
897 del self
.tagged_commands
[tag
]
900 # Some have reported "unexpected response" exceptions.
901 # Note that ignoring them here causes loops.
902 # Instead, send me details of the unexpected response and
903 # I'll update the code in `_get_response()'.
907 except self
.abort
, val
:
916 line
= self
.readline()
918 raise self
.abort('socket error: EOF')
920 # Protocol mandates all lines terminated by CRLF
925 self
._mesg
('< %s' % line
)
927 self
._log
('< %s' % line
)
931 def _match(self
, cre
, s
):
933 # Run compiled regular expression match method on 's'.
934 # Save result, return success.
936 self
.mo
= cre
.match(s
)
938 if self
.mo
is not None and self
.debug
>= 5:
939 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
940 return self
.mo
is not None
945 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
946 self
.tagnum
= self
.tagnum
+ 1
947 self
.tagged_commands
[tag
] = None
951 def _checkquote(self
, arg
):
953 # Must quote command args if non-alphanumeric chars present,
954 # and not already quoted.
956 if type(arg
) is not type(''):
958 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
960 if self
.mustquote
.search(arg
) is None:
962 return self
._quote
(arg
)
965 def _quote(self
, arg
):
967 arg
= arg
.replace('\\', '\\\\')
968 arg
= arg
.replace('"', '\\"')
973 def _simple_command(self
, name
, *args
):
975 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
978 def _untagged_response(self
, typ
, dat
, name
):
982 if not name
in self
.untagged_responses
:
984 data
= self
.untagged_responses
[name
]
987 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
988 del self
.untagged_responses
[name
]
994 def _mesg(self
, s
, secs
=None):
997 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
998 UIBase
.getglobalui().debug('imap', ' %s.%02d %s' % (tm
, (secs
*100)%100, s
))
1000 def _dump_ur(self
, dict):
1001 # Dump untagged responses (in `dict').
1005 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1006 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1008 def _log(self
, line
):
1009 # Keep log of last `_cmd_log_len' interactions for debugging.
1010 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1011 self
._cmd
_log
_idx
+= 1
1012 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1013 self
._cmd
_log
_idx
= 0
1015 def print_log(self
):
1016 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1017 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1020 apply(self
._mesg
, self
._cmd
_log
[i
])
1024 if i
>= self
._cmd
_log
_len
:
1028 class IMAP4_Tunnel(IMAP4
):
1029 """IMAP4 client class over a tunnel
1031 Instantiate with: IMAP4_Tunnel(tunnelcmd)
1033 tunnelcmd -- shell command to generate the tunnel.
1034 The result will be in PREAUTH stage."""
1036 def __init__(self
, tunnelcmd
):
1037 IMAP4
.__init__(self
, tunnelcmd
)
1039 def open(self
, host
, port
):
1040 """The tunnelcmd comes in on host!"""
1041 self
.outfd
, self
.infd
= os
.popen2(host
, "t", 0)
1043 def read(self
, size
):
1045 while len(retval
) < size
:
1046 retval
+= self
.infd
.read(size
- len(retval
))
1050 return self
.infd
.readline()
1052 def send(self
, data
):
1053 self
.outfd
.write(data
)
1061 def __init__(self
, sslsock
):
1062 self
.sslsock
= sslsock
1066 return self
.sslsock
.write(s
)
1069 return self
.sslsock
.read(n
)
1072 if len(self
.readbuf
):
1073 # Return the stuff in readbuf, even if less than n.
1074 # It might contain the rest of the line, and if we try to
1075 # read more, might block waiting for data that is not
1077 bytesfrombuf
= min(n
, len(self
.readbuf
))
1078 retval
= self
.readbuf
[:bytesfrombuf
]
1079 self
.readbuf
= self
.readbuf
[bytesfrombuf
:]
1081 retval
= self
._read
(n
)
1083 self
.readbuf
= retval
[n
:]
1090 linebuf
= self
.read(1024)
1091 nlindex
= linebuf
.find("\n")
1093 retval
+= linebuf
[:nlindex
+ 1]
1094 self
.readbuf
= linebuf
[nlindex
+ 1:] + self
.readbuf
1100 class IMAP4_SSL(IMAP4
):
1102 """IMAP4 client class over SSL connection
1104 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1106 host - host's name (default: localhost);
1107 port - port number (default: standard IMAP4 SSL port).
1108 keyfile - PEM formatted file that contains your private key (default: None);
1109 certfile - PEM formatted certificate chain file (default: None);
1111 for more documentation see the docstring of the parent class IMAP4.
1115 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1116 self
.keyfile
= keyfile
1117 self
.certfile
= certfile
1118 IMAP4
.__init__(self
, host
, port
)
1121 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1122 """Setup connection to remote server on "host:port".
1123 (default: localhost:standard IMAP4 SSL port).
1124 This connection will be used by the routines:
1125 read, readline, send, shutdown.
1129 #This connects to the first ip found ipv4/ipv6
1130 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
1131 #example from the python documentation:
1132 #http://www.python.org/doc/lib/socket-example.html
1133 res
= socket
.getaddrinfo(host
, port
, socket
.AF_UNSPEC
,
1135 af
, socktype
, proto
, canonname
, sa
= res
[0]
1136 self
.sock
= socket
.socket(af
, socktype
, proto
)
1137 self
.sock
.connect(sa
)
1138 if sys
.version_info
[0] <= 2 and sys
.version_info
[1] <= 2:
1139 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1141 self
.sslobj
= socket
.ssl(self
.sock
._sock
, self
.keyfile
, self
.certfile
)
1142 self
.sslobj
= sslwrapper(self
.sslobj
)
1145 def read(self
, size
):
1146 """Read 'size' bytes from remote."""
1148 while len(retval
) < size
:
1149 retval
+= self
.sslobj
.read(size
- len(retval
))
1154 """Read line from remote."""
1155 return self
.sslobj
.readline()
1157 def send(self
, data
):
1158 """Send data to remote."""
1160 bytestowrite
= len(data
)
1161 while byteswritten
< bytestowrite
:
1162 byteswritten
+= self
.sslobj
.write(data
[byteswritten
:])
1166 """Close I/O established in "open"."""
1171 """Return socket instance used to connect to IMAP4 server.
1173 socket = <instance>.socket()
1179 """Return SSLObject instance used to communicate with the IMAP4 server.
1181 ssl = <instance>.socket.ssl()
1187 class _Authenticator
:
1189 """Private class to provide en/decoding
1190 for base64-based authentication conversation.
1193 def __init__(self
, mechinst
):
1194 self
.mech
= mechinst
# Callable object to provide/process data
1196 def process(self
, data
):
1197 ret
= self
.mech(self
.decode(data
))
1199 return '*' # Abort conversation
1200 return self
.encode(ret
)
1202 def encode(self
, inp
):
1204 # Invoke binascii.b2a_base64 iteratively with
1205 # short even length buffers, strip the trailing
1206 # line feed from the result and append. "Even"
1207 # means a number that factors to both 6 and 8,
1208 # so when it gets to the end of the 8-bit input
1209 # there's no partial 6-bit output.
1219 e
= binascii
.b2a_base64(t
)
1224 def decode(self
, inp
):
1227 return binascii
.a2b_base64(inp
)
1231 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1232 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1234 def Internaldate2epoch(resp
):
1235 """Convert IMAP4 INTERNALDATE to UT.
1237 Returns seconds since the epoch.
1240 mo
= InternalDate
.match(resp
)
1244 mon
= Mon2num
[mo
.group('mon')]
1245 zonen
= mo
.group('zonen')
1247 day
= int(mo
.group('day'))
1248 year
= int(mo
.group('year'))
1249 hour
= int(mo
.group('hour'))
1250 min = int(mo
.group('min'))
1251 sec
= int(mo
.group('sec'))
1252 zoneh
= int(mo
.group('zoneh'))
1253 zonem
= int(mo
.group('zonem'))
1255 # INTERNALDATE timezone must be subtracted to get UT
1257 zone
= (zoneh
*60 + zonem
)*60
1261 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1263 return time
.mktime(tt
)
1266 def Internaldate2tuple(resp
):
1267 """Convert IMAP4 INTERNALDATE to UT.
1269 Returns Python time module tuple.
1272 utc
= Internaldate2epoch(resp
)
1274 # Following is necessary because the time module has no 'mkgmtime'.
1275 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1277 lt
= time
.localtime(utc
)
1278 if time
.daylight
and lt
[-1]:
1279 zone
= zone
+ time
.altzone
1281 zone
= zone
+ time
.timezone
1283 return time
.localtime(utc
- zone
)
1289 """Convert integer to A-P string representation."""
1291 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1294 num
, mod
= divmod(num
, 16)
1300 def ParseFlags(resp
):
1302 """Convert IMAP4 flags response to python tuple."""
1304 mo
= Flags
.match(resp
)
1308 return tuple(mo
.group('flags').split())
1311 def Time2Internaldate(date_time
):
1313 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1315 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1318 if isinstance(date_time
, (int, float)):
1319 tt
= time
.localtime(date_time
)
1320 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1322 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1323 return date_time
# Assume in correct format
1325 raise ValueError("date_time not of a known type")
1327 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1330 if time
.daylight
and tt
[-1]:
1331 zone
= -time
.altzone
1333 zone
= -time
.timezone
1334 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1338 if __name__
== '__main__':
1340 import getopt
, getpass
1343 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1344 except getopt
.error
, val
:
1347 for opt
,val
in optlist
:
1351 if not args
: args
= ('',)
1355 USER
= getpass
.getuser()
1356 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1358 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1360 ('login', (USER
, PASSWD
)),
1361 ('create', ('/tmp/xxx 1',)),
1362 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1363 ('CREATE', ('/tmp/yyz 2',)),
1364 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1365 ('list', ('/tmp', 'yy*')),
1366 ('select', ('/tmp/yyz 2',)),
1367 ('search', (None, 'SUBJECT', 'test')),
1368 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1369 ('store', ('1', 'FLAGS', '(\Deleted)')),
1378 ('response',('UIDVALIDITY',)),
1379 ('uid', ('SEARCH', 'ALL')),
1380 ('response', ('EXISTS',)),
1381 ('append', (None, None, None, test_mesg
)),
1387 M
._mesg
('%s %s' % (cmd
, args
))
1388 typ
, dat
= apply(getattr(M
, cmd
), args
)
1389 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1394 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1395 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1397 for cmd
,args
in test_seq1
:
1400 for ml
in run('list', ('/tmp/', 'yy%')):
1401 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1402 if mo
: path
= mo
.group(1)
1403 else: path
= ml
.split()[-1]
1404 run('delete', (path
,))
1406 for cmd
,args
in test_seq2
:
1407 dat
= run(cmd
, args
)
1409 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1412 uid
= dat
[-1].split()
1413 if not uid
: continue
1414 run('uid', ('FETCH', '%s' % uid
[-1],
1415 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1417 print '\nAll tests OK.'
1420 print '\nTests failed.'
1424 If you would like to see debugging output,