ยปCore Development>Code coverage>Lib/imaplib.py

Python code coverage for Lib/imaplib.py

#countcontent
1n/a"""IMAP4 client.
2n/a
3n/aBased on RFC 2060.
4n/a
5n/aPublic class: IMAP4
6n/aPublic variable: Debug
7n/aPublic functions: Internaldate2tuple
8n/a Int2AP
9n/a ParseFlags
10n/a Time2Internaldate
11n/a"""
12n/a
13n/a# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
14n/a#
15n/a# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16n/a# String method conversion by ESR, February 2001.
17n/a# GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
18n/a# IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
19n/a# GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
20n/a# PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
21n/a# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
22n/a
23n/a__version__ = "2.58"
24n/a
25n/aimport binascii, errno, random, re, socket, subprocess, sys, time, calendar
26n/afrom datetime import datetime, timezone, timedelta
27n/afrom io import DEFAULT_BUFFER_SIZE
28n/a
29n/atry:
30n/a import ssl
31n/a HAVE_SSL = True
32n/aexcept ImportError:
33n/a HAVE_SSL = False
34n/a
35n/a__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
36n/a "Int2AP", "ParseFlags", "Time2Internaldate"]
37n/a
38n/a# Globals
39n/a
40n/aCRLF = b'\r\n'
41n/aDebug = 0
42n/aIMAP4_PORT = 143
43n/aIMAP4_SSL_PORT = 993
44n/aAllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
45n/a
46n/a# Maximal line length when calling readline(). This is to prevent
47n/a# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1)
48n/a# don't specify a line length. RFC 2683 suggests limiting client
49n/a# command lines to 1000 octets and that servers should be prepared
50n/a# to accept command lines up to 8000 octets, so we used to use 10K here.
51n/a# In the modern world (eg: gmail) the response to, for example, a
52n/a# search command can be quite large, so we now use 1M.
53n/a_MAXLINE = 1000000
54n/a
55n/a
56n/a# Commands
57n/a
58n/aCommands = {
59n/a # name valid states
60n/a 'APPEND': ('AUTH', 'SELECTED'),
61n/a 'AUTHENTICATE': ('NONAUTH',),
62n/a 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
63n/a 'CHECK': ('SELECTED',),
64n/a 'CLOSE': ('SELECTED',),
65n/a 'COPY': ('SELECTED',),
66n/a 'CREATE': ('AUTH', 'SELECTED'),
67n/a 'DELETE': ('AUTH', 'SELECTED'),
68n/a 'DELETEACL': ('AUTH', 'SELECTED'),
69n/a 'ENABLE': ('AUTH', ),
70n/a 'EXAMINE': ('AUTH', 'SELECTED'),
71n/a 'EXPUNGE': ('SELECTED',),
72n/a 'FETCH': ('SELECTED',),
73n/a 'GETACL': ('AUTH', 'SELECTED'),
74n/a 'GETANNOTATION':('AUTH', 'SELECTED'),
75n/a 'GETQUOTA': ('AUTH', 'SELECTED'),
76n/a 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
77n/a 'MYRIGHTS': ('AUTH', 'SELECTED'),
78n/a 'LIST': ('AUTH', 'SELECTED'),
79n/a 'LOGIN': ('NONAUTH',),
80n/a 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
81n/a 'LSUB': ('AUTH', 'SELECTED'),
82n/a 'NAMESPACE': ('AUTH', 'SELECTED'),
83n/a 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
84n/a 'PARTIAL': ('SELECTED',), # NB: obsolete
85n/a 'PROXYAUTH': ('AUTH',),
86n/a 'RENAME': ('AUTH', 'SELECTED'),
87n/a 'SEARCH': ('SELECTED',),
88n/a 'SELECT': ('AUTH', 'SELECTED'),
89n/a 'SETACL': ('AUTH', 'SELECTED'),
90n/a 'SETANNOTATION':('AUTH', 'SELECTED'),
91n/a 'SETQUOTA': ('AUTH', 'SELECTED'),
92n/a 'SORT': ('SELECTED',),
93n/a 'STARTTLS': ('NONAUTH',),
94n/a 'STATUS': ('AUTH', 'SELECTED'),
95n/a 'STORE': ('SELECTED',),
96n/a 'SUBSCRIBE': ('AUTH', 'SELECTED'),
97n/a 'THREAD': ('SELECTED',),
98n/a 'UID': ('SELECTED',),
99n/a 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
100n/a }
101n/a
102n/a# Patterns to match server responses
103n/a
104n/aContinuation = re.compile(br'\+( (?P<data>.*))?')
105n/aFlags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')
106n/aInternalDate = re.compile(br'.*INTERNALDATE "'
107n/a br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
108n/a br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
109n/a br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
110n/a br'"')
111n/a# Literal is no longer used; kept for backward compatibility.
112n/aLiteral = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)
113n/aMapCRLF = re.compile(br'\r\n|\r|\n')
114n/a# We no longer exclude the ']' character from the data portion of the response
115n/a# code, even though it violates the RFC. Popular IMAP servers such as Gmail
116n/a# allow flags with ']', and there are programs (including imaplib!) that can
117n/a# produce them. The problem with this is if the 'text' portion of the response
118n/a# includes a ']' we'll parse the response wrong (which is the point of the RFC
119n/a# restriction). However, that seems less likely to be a problem in practice
120n/a# than being unable to correctly parse flags that include ']' chars, which
121n/a# was reported as a real-world problem in issue #21815.
122n/aResponse_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>.*))?\]')
123n/aUntagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
124n/a# Untagged_status is no longer used; kept for backward compatibility
125n/aUntagged_status = re.compile(
126n/a br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)
127n/a# We compile these in _mode_xxx.
128n/a_Literal = br'.*{(?P<size>\d+)}$'
129n/a_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
130n/a
131n/a
132n/a
133n/aclass IMAP4:
134n/a
135n/a r"""IMAP4 client class.
136n/a
137n/a Instantiate with: IMAP4([host[, port]])
138n/a
139n/a host - host's name (default: localhost);
140n/a port - port number (default: standard IMAP4 port).
141n/a
142n/a All IMAP4rev1 commands are supported by methods of the same
143n/a name (in lower-case).
144n/a
145n/a All arguments to commands are converted to strings, except for
146n/a AUTHENTICATE, and the last argument to APPEND which is passed as
147n/a an IMAP4 literal. If necessary (the string contains any
148n/a non-printing characters or white-space and isn't enclosed with
149n/a either parentheses or double quotes) each string is quoted.
150n/a However, the 'password' argument to the LOGIN command is always
151n/a quoted. If you want to avoid having an argument string quoted
152n/a (eg: the 'flags' argument to STORE) then enclose the string in
153n/a parentheses (eg: "(\Deleted)").
154n/a
155n/a Each command returns a tuple: (type, [data, ...]) where 'type'
156n/a is usually 'OK' or 'NO', and 'data' is either the text from the
157n/a tagged response, or untagged results from command. Each 'data'
158n/a is either a string, or a tuple. If a tuple, then the first part
159n/a is the header of the response, and the second part contains
160n/a the data (ie: 'literal' value).
161n/a
162n/a Errors raise the exception class <instance>.error("<reason>").
163n/a IMAP4 server errors raise <instance>.abort("<reason>"),
164n/a which is a sub-class of 'error'. Mailbox status changes
165n/a from READ-WRITE to READ-ONLY raise the exception class
166n/a <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
167n/a
168n/a "error" exceptions imply a program error.
169n/a "abort" exceptions imply the connection should be reset, and
170n/a the command re-tried.
171n/a "readonly" exceptions imply the command should be re-tried.
172n/a
173n/a Note: to use this module, you must read the RFCs pertaining to the
174n/a IMAP4 protocol, as the semantics of the arguments to each IMAP4
175n/a command are left to the invoker, not to mention the results. Also,
176n/a most IMAP servers implement a sub-set of the commands available here.
177n/a """
178n/a
179n/a class error(Exception): pass # Logical errors - debug required
180n/a class abort(error): pass # Service errors - close and retry
181n/a class readonly(abort): pass # Mailbox status changed to READ-ONLY
182n/a
183n/a def __init__(self, host='', port=IMAP4_PORT):
184n/a self.debug = Debug
185n/a self.state = 'LOGOUT'
186n/a self.literal = None # A literal argument to a command
187n/a self.tagged_commands = {} # Tagged commands awaiting response
188n/a self.untagged_responses = {} # {typ: [data, ...], ...}
189n/a self.continuation_response = '' # Last continuation response
190n/a self.is_readonly = False # READ-ONLY desired state
191n/a self.tagnum = 0
192n/a self._tls_established = False
193n/a self._mode_ascii()
194n/a
195n/a # Open socket to server.
196n/a
197n/a self.open(host, port)
198n/a
199n/a try:
200n/a self._connect()
201n/a except Exception:
202n/a try:
203n/a self.shutdown()
204n/a except OSError:
205n/a pass
206n/a raise
207n/a
208n/a def _mode_ascii(self):
209n/a self.utf8_enabled = False
210n/a self._encoding = 'ascii'
211n/a self.Literal = re.compile(_Literal, re.ASCII)
212n/a self.Untagged_status = re.compile(_Untagged_status, re.ASCII)
213n/a
214n/a
215n/a def _mode_utf8(self):
216n/a self.utf8_enabled = True
217n/a self._encoding = 'utf-8'
218n/a self.Literal = re.compile(_Literal)
219n/a self.Untagged_status = re.compile(_Untagged_status)
220n/a
221n/a
222n/a def _connect(self):
223n/a # Create unique tag for this session,
224n/a # and compile tagged response matcher.
225n/a
226n/a self.tagpre = Int2AP(random.randint(4096, 65535))
227n/a self.tagre = re.compile(br'(?P<tag>'
228n/a + self.tagpre
229n/a + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)
230n/a
231n/a # Get server welcome message,
232n/a # request and store CAPABILITY response.
233n/a
234n/a if __debug__:
235n/a self._cmd_log_len = 10
236n/a self._cmd_log_idx = 0
237n/a self._cmd_log = {} # Last `_cmd_log_len' interactions
238n/a if self.debug >= 1:
239n/a self._mesg('imaplib version %s' % __version__)
240n/a self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
241n/a
242n/a self.welcome = self._get_response()
243n/a if 'PREAUTH' in self.untagged_responses:
244n/a self.state = 'AUTH'
245n/a elif 'OK' in self.untagged_responses:
246n/a self.state = 'NONAUTH'
247n/a else:
248n/a raise self.error(self.welcome)
249n/a
250n/a self._get_capabilities()
251n/a if __debug__:
252n/a if self.debug >= 3:
253n/a self._mesg('CAPABILITIES: %r' % (self.capabilities,))
254n/a
255n/a for version in AllowedVersions:
256n/a if not version in self.capabilities:
257n/a continue
258n/a self.PROTOCOL_VERSION = version
259n/a return
260n/a
261n/a raise self.error('server not IMAP4 compliant')
262n/a
263n/a
264n/a def __getattr__(self, attr):
265n/a # Allow UPPERCASE variants of IMAP4 command methods.
266n/a if attr in Commands:
267n/a return getattr(self, attr.lower())
268n/a raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
269n/a
270n/a def __enter__(self):
271n/a return self
272n/a
273n/a def __exit__(self, *args):
274n/a try:
275n/a self.logout()
276n/a except OSError:
277n/a pass
278n/a
279n/a
280n/a # Overridable methods
281n/a
282n/a
283n/a def _create_socket(self):
284n/a return socket.create_connection((self.host, self.port))
285n/a
286n/a def open(self, host = '', port = IMAP4_PORT):
287n/a """Setup connection to remote server on "host:port"
288n/a (default: localhost:standard IMAP4 port).
289n/a This connection will be used by the routines:
290n/a read, readline, send, shutdown.
291n/a """
292n/a self.host = host
293n/a self.port = port
294n/a self.sock = self._create_socket()
295n/a self.file = self.sock.makefile('rb')
296n/a
297n/a
298n/a def read(self, size):
299n/a """Read 'size' bytes from remote."""
300n/a return self.file.read(size)
301n/a
302n/a
303n/a def readline(self):
304n/a """Read line from remote."""
305n/a line = self.file.readline(_MAXLINE + 1)
306n/a if len(line) > _MAXLINE:
307n/a raise self.error("got more than %d bytes" % _MAXLINE)
308n/a return line
309n/a
310n/a
311n/a def send(self, data):
312n/a """Send data to remote."""
313n/a self.sock.sendall(data)
314n/a
315n/a
316n/a def shutdown(self):
317n/a """Close I/O established in "open"."""
318n/a self.file.close()
319n/a try:
320n/a self.sock.shutdown(socket.SHUT_RDWR)
321n/a except OSError as e:
322n/a # The server might already have closed the connection
323n/a if e.errno != errno.ENOTCONN:
324n/a raise
325n/a finally:
326n/a self.sock.close()
327n/a
328n/a
329n/a def socket(self):
330n/a """Return socket instance used to connect to IMAP4 server.
331n/a
332n/a socket = <instance>.socket()
333n/a """
334n/a return self.sock
335n/a
336n/a
337n/a
338n/a # Utility methods
339n/a
340n/a
341n/a def recent(self):
342n/a """Return most recent 'RECENT' responses if any exist,
343n/a else prompt server for an update using the 'NOOP' command.
344n/a
345n/a (typ, [data]) = <instance>.recent()
346n/a
347n/a 'data' is None if no new messages,
348n/a else list of RECENT responses, most recent last.
349n/a """
350n/a name = 'RECENT'
351n/a typ, dat = self._untagged_response('OK', [None], name)
352n/a if dat[-1]:
353n/a return typ, dat
354n/a typ, dat = self.noop() # Prod server for response
355n/a return self._untagged_response(typ, dat, name)
356n/a
357n/a
358n/a def response(self, code):
359n/a """Return data for response 'code' if received, or None.
360n/a
361n/a Old value for response 'code' is cleared.
362n/a
363n/a (code, [data]) = <instance>.response(code)
364n/a """
365n/a return self._untagged_response(code, [None], code.upper())
366n/a
367n/a
368n/a
369n/a # IMAP4 commands
370n/a
371n/a
372n/a def append(self, mailbox, flags, date_time, message):
373n/a """Append message to named mailbox.
374n/a
375n/a (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
376n/a
377n/a All args except `message' can be None.
378n/a """
379n/a name = 'APPEND'
380n/a if not mailbox:
381n/a mailbox = 'INBOX'
382n/a if flags:
383n/a if (flags[0],flags[-1]) != ('(',')'):
384n/a flags = '(%s)' % flags
385n/a else:
386n/a flags = None
387n/a if date_time:
388n/a date_time = Time2Internaldate(date_time)
389n/a else:
390n/a date_time = None
391n/a literal = MapCRLF.sub(CRLF, message)
392n/a if self.utf8_enabled:
393n/a literal = b'UTF8 (' + literal + b')'
394n/a self.literal = literal
395n/a return self._simple_command(name, mailbox, flags, date_time)
396n/a
397n/a
398n/a def authenticate(self, mechanism, authobject):
399n/a """Authenticate command - requires response processing.
400n/a
401n/a 'mechanism' specifies which authentication mechanism is to
402n/a be used - it must appear in <instance>.capabilities in the
403n/a form AUTH=<mechanism>.
404n/a
405n/a 'authobject' must be a callable object:
406n/a
407n/a data = authobject(response)
408n/a
409n/a It will be called to process server continuation responses; the
410n/a response argument it is passed will be a bytes. It should return bytes
411n/a data that will be base64 encoded and sent to the server. It should
412n/a return None if the client abort response '*' should be sent instead.
413n/a """
414n/a mech = mechanism.upper()
415n/a # XXX: shouldn't this code be removed, not commented out?
416n/a #cap = 'AUTH=%s' % mech
417n/a #if not cap in self.capabilities: # Let the server decide!
418n/a # raise self.error("Server doesn't allow %s authentication." % mech)
419n/a self.literal = _Authenticator(authobject).process
420n/a typ, dat = self._simple_command('AUTHENTICATE', mech)
421n/a if typ != 'OK':
422n/a raise self.error(dat[-1].decode('utf-8', 'replace'))
423n/a self.state = 'AUTH'
424n/a return typ, dat
425n/a
426n/a
427n/a def capability(self):
428n/a """(typ, [data]) = <instance>.capability()
429n/a Fetch capabilities list from server."""
430n/a
431n/a name = 'CAPABILITY'
432n/a typ, dat = self._simple_command(name)
433n/a return self._untagged_response(typ, dat, name)
434n/a
435n/a
436n/a def check(self):
437n/a """Checkpoint mailbox on server.
438n/a
439n/a (typ, [data]) = <instance>.check()
440n/a """
441n/a return self._simple_command('CHECK')
442n/a
443n/a
444n/a def close(self):
445n/a """Close currently selected mailbox.
446n/a
447n/a Deleted messages are removed from writable mailbox.
448n/a This is the recommended command before 'LOGOUT'.
449n/a
450n/a (typ, [data]) = <instance>.close()
451n/a """
452n/a try:
453n/a typ, dat = self._simple_command('CLOSE')
454n/a finally:
455n/a self.state = 'AUTH'
456n/a return typ, dat
457n/a
458n/a
459n/a def copy(self, message_set, new_mailbox):
460n/a """Copy 'message_set' messages onto end of 'new_mailbox'.
461n/a
462n/a (typ, [data]) = <instance>.copy(message_set, new_mailbox)
463n/a """
464n/a return self._simple_command('COPY', message_set, new_mailbox)
465n/a
466n/a
467n/a def create(self, mailbox):
468n/a """Create new mailbox.
469n/a
470n/a (typ, [data]) = <instance>.create(mailbox)
471n/a """
472n/a return self._simple_command('CREATE', mailbox)
473n/a
474n/a
475n/a def delete(self, mailbox):
476n/a """Delete old mailbox.
477n/a
478n/a (typ, [data]) = <instance>.delete(mailbox)
479n/a """
480n/a return self._simple_command('DELETE', mailbox)
481n/a
482n/a def deleteacl(self, mailbox, who):
483n/a """Delete the ACLs (remove any rights) set for who on mailbox.
484n/a
485n/a (typ, [data]) = <instance>.deleteacl(mailbox, who)
486n/a """
487n/a return self._simple_command('DELETEACL', mailbox, who)
488n/a
489n/a def enable(self, capability):
490n/a """Send an RFC5161 enable string to the server.
491n/a
492n/a (typ, [data]) = <intance>.enable(capability)
493n/a """
494n/a if 'ENABLE' not in self.capabilities:
495n/a raise IMAP4.error("Server does not support ENABLE")
496n/a typ, data = self._simple_command('ENABLE', capability)
497n/a if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():
498n/a self._mode_utf8()
499n/a return typ, data
500n/a
501n/a def expunge(self):
502n/a """Permanently remove deleted items from selected mailbox.
503n/a
504n/a Generates 'EXPUNGE' response for each deleted message.
505n/a
506n/a (typ, [data]) = <instance>.expunge()
507n/a
508n/a 'data' is list of 'EXPUNGE'd message numbers in order received.
509n/a """
510n/a name = 'EXPUNGE'
511n/a typ, dat = self._simple_command(name)
512n/a return self._untagged_response(typ, dat, name)
513n/a
514n/a
515n/a def fetch(self, message_set, message_parts):
516n/a """Fetch (parts of) messages.
517n/a
518n/a (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
519n/a
520n/a 'message_parts' should be a string of selected parts
521n/a enclosed in parentheses, eg: "(UID BODY[TEXT])".
522n/a
523n/a 'data' are tuples of message part envelope and data.
524n/a """
525n/a name = 'FETCH'
526n/a typ, dat = self._simple_command(name, message_set, message_parts)
527n/a return self._untagged_response(typ, dat, name)
528n/a
529n/a
530n/a def getacl(self, mailbox):
531n/a """Get the ACLs for a mailbox.
532n/a
533n/a (typ, [data]) = <instance>.getacl(mailbox)
534n/a """
535n/a typ, dat = self._simple_command('GETACL', mailbox)
536n/a return self._untagged_response(typ, dat, 'ACL')
537n/a
538n/a
539n/a def getannotation(self, mailbox, entry, attribute):
540n/a """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
541n/a Retrieve ANNOTATIONs."""
542n/a
543n/a typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
544n/a return self._untagged_response(typ, dat, 'ANNOTATION')
545n/a
546n/a
547n/a def getquota(self, root):
548n/a """Get the quota root's resource usage and limits.
549n/a
550n/a Part of the IMAP4 QUOTA extension defined in rfc2087.
551n/a
552n/a (typ, [data]) = <instance>.getquota(root)
553n/a """
554n/a typ, dat = self._simple_command('GETQUOTA', root)
555n/a return self._untagged_response(typ, dat, 'QUOTA')
556n/a
557n/a
558n/a def getquotaroot(self, mailbox):
559n/a """Get the list of quota roots for the named mailbox.
560n/a
561n/a (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
562n/a """
563n/a typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
564n/a typ, quota = self._untagged_response(typ, dat, 'QUOTA')
565n/a typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
566n/a return typ, [quotaroot, quota]
567n/a
568n/a
569n/a def list(self, directory='""', pattern='*'):
570n/a """List mailbox names in directory matching pattern.
571n/a
572n/a (typ, [data]) = <instance>.list(directory='""', pattern='*')
573n/a
574n/a 'data' is list of LIST responses.
575n/a """
576n/a name = 'LIST'
577n/a typ, dat = self._simple_command(name, directory, pattern)
578n/a return self._untagged_response(typ, dat, name)
579n/a
580n/a
581n/a def login(self, user, password):
582n/a """Identify client using plaintext password.
583n/a
584n/a (typ, [data]) = <instance>.login(user, password)
585n/a
586n/a NB: 'password' will be quoted.
587n/a """
588n/a typ, dat = self._simple_command('LOGIN', user, self._quote(password))
589n/a if typ != 'OK':
590n/a raise self.error(dat[-1])
591n/a self.state = 'AUTH'
592n/a return typ, dat
593n/a
594n/a
595n/a def login_cram_md5(self, user, password):
596n/a """ Force use of CRAM-MD5 authentication.
597n/a
598n/a (typ, [data]) = <instance>.login_cram_md5(user, password)
599n/a """
600n/a self.user, self.password = user, password
601n/a return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
602n/a
603n/a
604n/a def _CRAM_MD5_AUTH(self, challenge):
605n/a """ Authobject to use with CRAM-MD5 authentication. """
606n/a import hmac
607n/a pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
608n/a else self.password)
609n/a return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
610n/a
611n/a
612n/a def logout(self):
613n/a """Shutdown connection to server.
614n/a
615n/a (typ, [data]) = <instance>.logout()
616n/a
617n/a Returns server 'BYE' response.
618n/a """
619n/a self.state = 'LOGOUT'
620n/a try: typ, dat = self._simple_command('LOGOUT')
621n/a except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
622n/a self.shutdown()
623n/a if 'BYE' in self.untagged_responses:
624n/a return 'BYE', self.untagged_responses['BYE']
625n/a return typ, dat
626n/a
627n/a
628n/a def lsub(self, directory='""', pattern='*'):
629n/a """List 'subscribed' mailbox names in directory matching pattern.
630n/a
631n/a (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
632n/a
633n/a 'data' are tuples of message part envelope and data.
634n/a """
635n/a name = 'LSUB'
636n/a typ, dat = self._simple_command(name, directory, pattern)
637n/a return self._untagged_response(typ, dat, name)
638n/a
639n/a def myrights(self, mailbox):
640n/a """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
641n/a
642n/a (typ, [data]) = <instance>.myrights(mailbox)
643n/a """
644n/a typ,dat = self._simple_command('MYRIGHTS', mailbox)
645n/a return self._untagged_response(typ, dat, 'MYRIGHTS')
646n/a
647n/a def namespace(self):
648n/a """ Returns IMAP namespaces ala rfc2342
649n/a
650n/a (typ, [data, ...]) = <instance>.namespace()
651n/a """
652n/a name = 'NAMESPACE'
653n/a typ, dat = self._simple_command(name)
654n/a return self._untagged_response(typ, dat, name)
655n/a
656n/a
657n/a def noop(self):
658n/a """Send NOOP command.
659n/a
660n/a (typ, [data]) = <instance>.noop()
661n/a """
662n/a if __debug__:
663n/a if self.debug >= 3:
664n/a self._dump_ur(self.untagged_responses)
665n/a return self._simple_command('NOOP')
666n/a
667n/a
668n/a def partial(self, message_num, message_part, start, length):
669n/a """Fetch truncated part of a message.
670n/a
671n/a (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
672n/a
673n/a 'data' is tuple of message part envelope and data.
674n/a """
675n/a name = 'PARTIAL'
676n/a typ, dat = self._simple_command(name, message_num, message_part, start, length)
677n/a return self._untagged_response(typ, dat, 'FETCH')
678n/a
679n/a
680n/a def proxyauth(self, user):
681n/a """Assume authentication as "user".
682n/a
683n/a Allows an authorised administrator to proxy into any user's
684n/a mailbox.
685n/a
686n/a (typ, [data]) = <instance>.proxyauth(user)
687n/a """
688n/a
689n/a name = 'PROXYAUTH'
690n/a return self._simple_command('PROXYAUTH', user)
691n/a
692n/a
693n/a def rename(self, oldmailbox, newmailbox):
694n/a """Rename old mailbox name to new.
695n/a
696n/a (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
697n/a """
698n/a return self._simple_command('RENAME', oldmailbox, newmailbox)
699n/a
700n/a
701n/a def search(self, charset, *criteria):
702n/a """Search mailbox for matching messages.
703n/a
704n/a (typ, [data]) = <instance>.search(charset, criterion, ...)
705n/a
706n/a 'data' is space separated list of matching message numbers.
707n/a If UTF8 is enabled, charset MUST be None.
708n/a """
709n/a name = 'SEARCH'
710n/a if charset:
711n/a if self.utf8_enabled:
712n/a raise IMAP4.error("Non-None charset not valid in UTF8 mode")
713n/a typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
714n/a else:
715n/a typ, dat = self._simple_command(name, *criteria)
716n/a return self._untagged_response(typ, dat, name)
717n/a
718n/a
719n/a def select(self, mailbox='INBOX', readonly=False):
720n/a """Select a mailbox.
721n/a
722n/a Flush all untagged responses.
723n/a
724n/a (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
725n/a
726n/a 'data' is count of messages in mailbox ('EXISTS' response).
727n/a
728n/a Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
729n/a other responses should be obtained via <instance>.response('FLAGS') etc.
730n/a """
731n/a self.untagged_responses = {} # Flush old responses.
732n/a self.is_readonly = readonly
733n/a if readonly:
734n/a name = 'EXAMINE'
735n/a else:
736n/a name = 'SELECT'
737n/a typ, dat = self._simple_command(name, mailbox)
738n/a if typ != 'OK':
739n/a self.state = 'AUTH' # Might have been 'SELECTED'
740n/a return typ, dat
741n/a self.state = 'SELECTED'
742n/a if 'READ-ONLY' in self.untagged_responses \
743n/a and not readonly:
744n/a if __debug__:
745n/a if self.debug >= 1:
746n/a self._dump_ur(self.untagged_responses)
747n/a raise self.readonly('%s is not writable' % mailbox)
748n/a return typ, self.untagged_responses.get('EXISTS', [None])
749n/a
750n/a
751n/a def setacl(self, mailbox, who, what):
752n/a """Set a mailbox acl.
753n/a
754n/a (typ, [data]) = <instance>.setacl(mailbox, who, what)
755n/a """
756n/a return self._simple_command('SETACL', mailbox, who, what)
757n/a
758n/a
759n/a def setannotation(self, *args):
760n/a """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
761n/a Set ANNOTATIONs."""
762n/a
763n/a typ, dat = self._simple_command('SETANNOTATION', *args)
764n/a return self._untagged_response(typ, dat, 'ANNOTATION')
765n/a
766n/a
767n/a def setquota(self, root, limits):
768n/a """Set the quota root's resource limits.
769n/a
770n/a (typ, [data]) = <instance>.setquota(root, limits)
771n/a """
772n/a typ, dat = self._simple_command('SETQUOTA', root, limits)
773n/a return self._untagged_response(typ, dat, 'QUOTA')
774n/a
775n/a
776n/a def sort(self, sort_criteria, charset, *search_criteria):
777n/a """IMAP4rev1 extension SORT command.
778n/a
779n/a (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
780n/a """
781n/a name = 'SORT'
782n/a #if not name in self.capabilities: # Let the server decide!
783n/a # raise self.error('unimplemented extension command: %s' % name)
784n/a if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
785n/a sort_criteria = '(%s)' % sort_criteria
786n/a typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
787n/a return self._untagged_response(typ, dat, name)
788n/a
789n/a
790n/a def starttls(self, ssl_context=None):
791n/a name = 'STARTTLS'
792n/a if not HAVE_SSL:
793n/a raise self.error('SSL support missing')
794n/a if self._tls_established:
795n/a raise self.abort('TLS session already established')
796n/a if name not in self.capabilities:
797n/a raise self.abort('TLS not supported by server')
798n/a # Generate a default SSL context if none was passed.
799n/a if ssl_context is None:
800n/a ssl_context = ssl._create_stdlib_context()
801n/a typ, dat = self._simple_command(name)
802n/a if typ == 'OK':
803n/a self.sock = ssl_context.wrap_socket(self.sock,
804n/a server_hostname=self.host)
805n/a self.file = self.sock.makefile('rb')
806n/a self._tls_established = True
807n/a self._get_capabilities()
808n/a else:
809n/a raise self.error("Couldn't establish TLS session")
810n/a return self._untagged_response(typ, dat, name)
811n/a
812n/a
813n/a def status(self, mailbox, names):
814n/a """Request named status conditions for mailbox.
815n/a
816n/a (typ, [data]) = <instance>.status(mailbox, names)
817n/a """
818n/a name = 'STATUS'
819n/a #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
820n/a # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
821n/a typ, dat = self._simple_command(name, mailbox, names)
822n/a return self._untagged_response(typ, dat, name)
823n/a
824n/a
825n/a def store(self, message_set, command, flags):
826n/a """Alters flag dispositions for messages in mailbox.
827n/a
828n/a (typ, [data]) = <instance>.store(message_set, command, flags)
829n/a """
830n/a if (flags[0],flags[-1]) != ('(',')'):
831n/a flags = '(%s)' % flags # Avoid quoting the flags
832n/a typ, dat = self._simple_command('STORE', message_set, command, flags)
833n/a return self._untagged_response(typ, dat, 'FETCH')
834n/a
835n/a
836n/a def subscribe(self, mailbox):
837n/a """Subscribe to new mailbox.
838n/a
839n/a (typ, [data]) = <instance>.subscribe(mailbox)
840n/a """
841n/a return self._simple_command('SUBSCRIBE', mailbox)
842n/a
843n/a
844n/a def thread(self, threading_algorithm, charset, *search_criteria):
845n/a """IMAPrev1 extension THREAD command.
846n/a
847n/a (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
848n/a """
849n/a name = 'THREAD'
850n/a typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
851n/a return self._untagged_response(typ, dat, name)
852n/a
853n/a
854n/a def uid(self, command, *args):
855n/a """Execute "command arg ..." with messages identified by UID,
856n/a rather than message number.
857n/a
858n/a (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
859n/a
860n/a Returns response appropriate to 'command'.
861n/a """
862n/a command = command.upper()
863n/a if not command in Commands:
864n/a raise self.error("Unknown IMAP4 UID command: %s" % command)
865n/a if self.state not in Commands[command]:
866n/a raise self.error("command %s illegal in state %s, "
867n/a "only allowed in states %s" %
868n/a (command, self.state,
869n/a ', '.join(Commands[command])))
870n/a name = 'UID'
871n/a typ, dat = self._simple_command(name, command, *args)
872n/a if command in ('SEARCH', 'SORT', 'THREAD'):
873n/a name = command
874n/a else:
875n/a name = 'FETCH'
876n/a return self._untagged_response(typ, dat, name)
877n/a
878n/a
879n/a def unsubscribe(self, mailbox):
880n/a """Unsubscribe from old mailbox.
881n/a
882n/a (typ, [data]) = <instance>.unsubscribe(mailbox)
883n/a """
884n/a return self._simple_command('UNSUBSCRIBE', mailbox)
885n/a
886n/a
887n/a def xatom(self, name, *args):
888n/a """Allow simple extension commands
889n/a notified by server in CAPABILITY response.
890n/a
891n/a Assumes command is legal in current state.
892n/a
893n/a (typ, [data]) = <instance>.xatom(name, arg, ...)
894n/a
895n/a Returns response appropriate to extension command `name'.
896n/a """
897n/a name = name.upper()
898n/a #if not name in self.capabilities: # Let the server decide!
899n/a # raise self.error('unknown extension command: %s' % name)
900n/a if not name in Commands:
901n/a Commands[name] = (self.state,)
902n/a return self._simple_command(name, *args)
903n/a
904n/a
905n/a
906n/a # Private methods
907n/a
908n/a
909n/a def _append_untagged(self, typ, dat):
910n/a if dat is None:
911n/a dat = b''
912n/a ur = self.untagged_responses
913n/a if __debug__:
914n/a if self.debug >= 5:
915n/a self._mesg('untagged_responses[%s] %s += ["%r"]' %
916n/a (typ, len(ur.get(typ,'')), dat))
917n/a if typ in ur:
918n/a ur[typ].append(dat)
919n/a else:
920n/a ur[typ] = [dat]
921n/a
922n/a
923n/a def _check_bye(self):
924n/a bye = self.untagged_responses.get('BYE')
925n/a if bye:
926n/a raise self.abort(bye[-1].decode(self._encoding, 'replace'))
927n/a
928n/a
929n/a def _command(self, name, *args):
930n/a
931n/a if self.state not in Commands[name]:
932n/a self.literal = None
933n/a raise self.error("command %s illegal in state %s, "
934n/a "only allowed in states %s" %
935n/a (name, self.state,
936n/a ', '.join(Commands[name])))
937n/a
938n/a for typ in ('OK', 'NO', 'BAD'):
939n/a if typ in self.untagged_responses:
940n/a del self.untagged_responses[typ]
941n/a
942n/a if 'READ-ONLY' in self.untagged_responses \
943n/a and not self.is_readonly:
944n/a raise self.readonly('mailbox status changed to READ-ONLY')
945n/a
946n/a tag = self._new_tag()
947n/a name = bytes(name, self._encoding)
948n/a data = tag + b' ' + name
949n/a for arg in args:
950n/a if arg is None: continue
951n/a if isinstance(arg, str):
952n/a arg = bytes(arg, self._encoding)
953n/a data = data + b' ' + arg
954n/a
955n/a literal = self.literal
956n/a if literal is not None:
957n/a self.literal = None
958n/a if type(literal) is type(self._command):
959n/a literator = literal
960n/a else:
961n/a literator = None
962n/a data = data + bytes(' {%s}' % len(literal), self._encoding)
963n/a
964n/a if __debug__:
965n/a if self.debug >= 4:
966n/a self._mesg('> %r' % data)
967n/a else:
968n/a self._log('> %r' % data)
969n/a
970n/a try:
971n/a self.send(data + CRLF)
972n/a except OSError as val:
973n/a raise self.abort('socket error: %s' % val)
974n/a
975n/a if literal is None:
976n/a return tag
977n/a
978n/a while 1:
979n/a # Wait for continuation response
980n/a
981n/a while self._get_response():
982n/a if self.tagged_commands[tag]: # BAD/NO?
983n/a return tag
984n/a
985n/a # Send literal
986n/a
987n/a if literator:
988n/a literal = literator(self.continuation_response)
989n/a
990n/a if __debug__:
991n/a if self.debug >= 4:
992n/a self._mesg('write literal size %s' % len(literal))
993n/a
994n/a try:
995n/a self.send(literal)
996n/a self.send(CRLF)
997n/a except OSError as val:
998n/a raise self.abort('socket error: %s' % val)
999n/a
1000n/a if not literator:
1001n/a break
1002n/a
1003n/a return tag
1004n/a
1005n/a
1006n/a def _command_complete(self, name, tag):
1007n/a # BYE is expected after LOGOUT
1008n/a if name != 'LOGOUT':
1009n/a self._check_bye()
1010n/a try:
1011n/a typ, data = self._get_tagged_response(tag)
1012n/a except self.abort as val:
1013n/a raise self.abort('command: %s => %s' % (name, val))
1014n/a except self.error as val:
1015n/a raise self.error('command: %s => %s' % (name, val))
1016n/a if name != 'LOGOUT':
1017n/a self._check_bye()
1018n/a if typ == 'BAD':
1019n/a raise self.error('%s command error: %s %s' % (name, typ, data))
1020n/a return typ, data
1021n/a
1022n/a
1023n/a def _get_capabilities(self):
1024n/a typ, dat = self.capability()
1025n/a if dat == [None]:
1026n/a raise self.error('no CAPABILITY response from server')
1027n/a dat = str(dat[-1], self._encoding)
1028n/a dat = dat.upper()
1029n/a self.capabilities = tuple(dat.split())
1030n/a
1031n/a
1032n/a def _get_response(self):
1033n/a
1034n/a # Read response and store.
1035n/a #
1036n/a # Returns None for continuation responses,
1037n/a # otherwise first response line received.
1038n/a
1039n/a resp = self._get_line()
1040n/a
1041n/a # Command completion response?
1042n/a
1043n/a if self._match(self.tagre, resp):
1044n/a tag = self.mo.group('tag')
1045n/a if not tag in self.tagged_commands:
1046n/a raise self.abort('unexpected tagged response: %r' % resp)
1047n/a
1048n/a typ = self.mo.group('type')
1049n/a typ = str(typ, self._encoding)
1050n/a dat = self.mo.group('data')
1051n/a self.tagged_commands[tag] = (typ, [dat])
1052n/a else:
1053n/a dat2 = None
1054n/a
1055n/a # '*' (untagged) responses?
1056n/a
1057n/a if not self._match(Untagged_response, resp):
1058n/a if self._match(self.Untagged_status, resp):
1059n/a dat2 = self.mo.group('data2')
1060n/a
1061n/a if self.mo is None:
1062n/a # Only other possibility is '+' (continuation) response...
1063n/a
1064n/a if self._match(Continuation, resp):
1065n/a self.continuation_response = self.mo.group('data')
1066n/a return None # NB: indicates continuation
1067n/a
1068n/a raise self.abort("unexpected response: %r" % resp)
1069n/a
1070n/a typ = self.mo.group('type')
1071n/a typ = str(typ, self._encoding)
1072n/a dat = self.mo.group('data')
1073n/a if dat is None: dat = b'' # Null untagged response
1074n/a if dat2: dat = dat + b' ' + dat2
1075n/a
1076n/a # Is there a literal to come?
1077n/a
1078n/a while self._match(self.Literal, dat):
1079n/a
1080n/a # Read literal direct from connection.
1081n/a
1082n/a size = int(self.mo.group('size'))
1083n/a if __debug__:
1084n/a if self.debug >= 4:
1085n/a self._mesg('read literal size %s' % size)
1086n/a data = self.read(size)
1087n/a
1088n/a # Store response with literal as tuple
1089n/a
1090n/a self._append_untagged(typ, (dat, data))
1091n/a
1092n/a # Read trailer - possibly containing another literal
1093n/a
1094n/a dat = self._get_line()
1095n/a
1096n/a self._append_untagged(typ, dat)
1097n/a
1098n/a # Bracketed response information?
1099n/a
1100n/a if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
1101n/a typ = self.mo.group('type')
1102n/a typ = str(typ, self._encoding)
1103n/a self._append_untagged(typ, self.mo.group('data'))
1104n/a
1105n/a if __debug__:
1106n/a if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
1107n/a self._mesg('%s response: %r' % (typ, dat))
1108n/a
1109n/a return resp
1110n/a
1111n/a
1112n/a def _get_tagged_response(self, tag):
1113n/a
1114n/a while 1:
1115n/a result = self.tagged_commands[tag]
1116n/a if result is not None:
1117n/a del self.tagged_commands[tag]
1118n/a return result
1119n/a
1120n/a # If we've seen a BYE at this point, the socket will be
1121n/a # closed, so report the BYE now.
1122n/a
1123n/a self._check_bye()
1124n/a
1125n/a # Some have reported "unexpected response" exceptions.
1126n/a # Note that ignoring them here causes loops.
1127n/a # Instead, send me details of the unexpected response and
1128n/a # I'll update the code in `_get_response()'.
1129n/a
1130n/a try:
1131n/a self._get_response()
1132n/a except self.abort as val:
1133n/a if __debug__:
1134n/a if self.debug >= 1:
1135n/a self.print_log()
1136n/a raise
1137n/a
1138n/a
1139n/a def _get_line(self):
1140n/a
1141n/a line = self.readline()
1142n/a if not line:
1143n/a raise self.abort('socket error: EOF')
1144n/a
1145n/a # Protocol mandates all lines terminated by CRLF
1146n/a if not line.endswith(b'\r\n'):
1147n/a raise self.abort('socket error: unterminated line: %r' % line)
1148n/a
1149n/a line = line[:-2]
1150n/a if __debug__:
1151n/a if self.debug >= 4:
1152n/a self._mesg('< %r' % line)
1153n/a else:
1154n/a self._log('< %r' % line)
1155n/a return line
1156n/a
1157n/a
1158n/a def _match(self, cre, s):
1159n/a
1160n/a # Run compiled regular expression match method on 's'.
1161n/a # Save result, return success.
1162n/a
1163n/a self.mo = cre.match(s)
1164n/a if __debug__:
1165n/a if self.mo is not None and self.debug >= 5:
1166n/a self._mesg("\tmatched r'%r' => %r" % (cre.pattern, self.mo.groups()))
1167n/a return self.mo is not None
1168n/a
1169n/a
1170n/a def _new_tag(self):
1171n/a
1172n/a tag = self.tagpre + bytes(str(self.tagnum), self._encoding)
1173n/a self.tagnum = self.tagnum + 1
1174n/a self.tagged_commands[tag] = None
1175n/a return tag
1176n/a
1177n/a
1178n/a def _quote(self, arg):
1179n/a
1180n/a arg = arg.replace('\\', '\\\\')
1181n/a arg = arg.replace('"', '\\"')
1182n/a
1183n/a return '"' + arg + '"'
1184n/a
1185n/a
1186n/a def _simple_command(self, name, *args):
1187n/a
1188n/a return self._command_complete(name, self._command(name, *args))
1189n/a
1190n/a
1191n/a def _untagged_response(self, typ, dat, name):
1192n/a if typ == 'NO':
1193n/a return typ, dat
1194n/a if not name in self.untagged_responses:
1195n/a return typ, [None]
1196n/a data = self.untagged_responses.pop(name)
1197n/a if __debug__:
1198n/a if self.debug >= 5:
1199n/a self._mesg('untagged_responses[%s] => %s' % (name, data))
1200n/a return typ, data
1201n/a
1202n/a
1203n/a if __debug__:
1204n/a
1205n/a def _mesg(self, s, secs=None):
1206n/a if secs is None:
1207n/a secs = time.time()
1208n/a tm = time.strftime('%M:%S', time.localtime(secs))
1209n/a sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1210n/a sys.stderr.flush()
1211n/a
1212n/a def _dump_ur(self, dict):
1213n/a # Dump untagged responses (in `dict').
1214n/a l = dict.items()
1215n/a if not l: return
1216n/a t = '\n\t\t'
1217n/a l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1218n/a self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1219n/a
1220n/a def _log(self, line):
1221n/a # Keep log of last `_cmd_log_len' interactions for debugging.
1222n/a self._cmd_log[self._cmd_log_idx] = (line, time.time())
1223n/a self._cmd_log_idx += 1
1224n/a if self._cmd_log_idx >= self._cmd_log_len:
1225n/a self._cmd_log_idx = 0
1226n/a
1227n/a def print_log(self):
1228n/a self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1229n/a i, n = self._cmd_log_idx, self._cmd_log_len
1230n/a while n:
1231n/a try:
1232n/a self._mesg(*self._cmd_log[i])
1233n/a except:
1234n/a pass
1235n/a i += 1
1236n/a if i >= self._cmd_log_len:
1237n/a i = 0
1238n/a n -= 1
1239n/a
1240n/a
1241n/aif HAVE_SSL:
1242n/a
1243n/a class IMAP4_SSL(IMAP4):
1244n/a
1245n/a """IMAP4 client class over SSL connection
1246n/a
1247n/a Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context]]]]])
1248n/a
1249n/a host - host's name (default: localhost);
1250n/a port - port number (default: standard IMAP4 SSL port);
1251n/a keyfile - PEM formatted file that contains your private key (default: None);
1252n/a certfile - PEM formatted certificate chain file (default: None);
1253n/a ssl_context - a SSLContext object that contains your certificate chain
1254n/a and private key (default: None)
1255n/a Note: if ssl_context is provided, then parameters keyfile or
1256n/a certfile should not be set otherwise ValueError is raised.
1257n/a
1258n/a for more documentation see the docstring of the parent class IMAP4.
1259n/a """
1260n/a
1261n/a
1262n/a def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
1263n/a certfile=None, ssl_context=None):
1264n/a if ssl_context is not None and keyfile is not None:
1265n/a raise ValueError("ssl_context and keyfile arguments are mutually "
1266n/a "exclusive")
1267n/a if ssl_context is not None and certfile is not None:
1268n/a raise ValueError("ssl_context and certfile arguments are mutually "
1269n/a "exclusive")
1270n/a if keyfile is not None or certfile is not None:
1271n/a import warnings
1272n/a warnings.warn("keyfile and certfile are deprecated, use a"
1273n/a "custom ssl_context instead", DeprecationWarning, 2)
1274n/a self.keyfile = keyfile
1275n/a self.certfile = certfile
1276n/a if ssl_context is None:
1277n/a ssl_context = ssl._create_stdlib_context(certfile=certfile,
1278n/a keyfile=keyfile)
1279n/a self.ssl_context = ssl_context
1280n/a IMAP4.__init__(self, host, port)
1281n/a
1282n/a def _create_socket(self):
1283n/a sock = IMAP4._create_socket(self)
1284n/a return self.ssl_context.wrap_socket(sock,
1285n/a server_hostname=self.host)
1286n/a
1287n/a def open(self, host='', port=IMAP4_SSL_PORT):
1288n/a """Setup connection to remote server on "host:port".
1289n/a (default: localhost:standard IMAP4 SSL port).
1290n/a This connection will be used by the routines:
1291n/a read, readline, send, shutdown.
1292n/a """
1293n/a IMAP4.open(self, host, port)
1294n/a
1295n/a __all__.append("IMAP4_SSL")
1296n/a
1297n/a
1298n/aclass IMAP4_stream(IMAP4):
1299n/a
1300n/a """IMAP4 client class over a stream
1301n/a
1302n/a Instantiate with: IMAP4_stream(command)
1303n/a
1304n/a "command" - a string that can be passed to subprocess.Popen()
1305n/a
1306n/a for more documentation see the docstring of the parent class IMAP4.
1307n/a """
1308n/a
1309n/a
1310n/a def __init__(self, command):
1311n/a self.command = command
1312n/a IMAP4.__init__(self)
1313n/a
1314n/a
1315n/a def open(self, host = None, port = None):
1316n/a """Setup a stream connection.
1317n/a This connection will be used by the routines:
1318n/a read, readline, send, shutdown.
1319n/a """
1320n/a self.host = None # For compatibility with parent class
1321n/a self.port = None
1322n/a self.sock = None
1323n/a self.file = None
1324n/a self.process = subprocess.Popen(self.command,
1325n/a bufsize=DEFAULT_BUFFER_SIZE,
1326n/a stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1327n/a shell=True, close_fds=True)
1328n/a self.writefile = self.process.stdin
1329n/a self.readfile = self.process.stdout
1330n/a
1331n/a def read(self, size):
1332n/a """Read 'size' bytes from remote."""
1333n/a return self.readfile.read(size)
1334n/a
1335n/a
1336n/a def readline(self):
1337n/a """Read line from remote."""
1338n/a return self.readfile.readline()
1339n/a
1340n/a
1341n/a def send(self, data):
1342n/a """Send data to remote."""
1343n/a self.writefile.write(data)
1344n/a self.writefile.flush()
1345n/a
1346n/a
1347n/a def shutdown(self):
1348n/a """Close I/O established in "open"."""
1349n/a self.readfile.close()
1350n/a self.writefile.close()
1351n/a self.process.wait()
1352n/a
1353n/a
1354n/a
1355n/aclass _Authenticator:
1356n/a
1357n/a """Private class to provide en/decoding
1358n/a for base64-based authentication conversation.
1359n/a """
1360n/a
1361n/a def __init__(self, mechinst):
1362n/a self.mech = mechinst # Callable object to provide/process data
1363n/a
1364n/a def process(self, data):
1365n/a ret = self.mech(self.decode(data))
1366n/a if ret is None:
1367n/a return b'*' # Abort conversation
1368n/a return self.encode(ret)
1369n/a
1370n/a def encode(self, inp):
1371n/a #
1372n/a # Invoke binascii.b2a_base64 iteratively with
1373n/a # short even length buffers, strip the trailing
1374n/a # line feed from the result and append. "Even"
1375n/a # means a number that factors to both 6 and 8,
1376n/a # so when it gets to the end of the 8-bit input
1377n/a # there's no partial 6-bit output.
1378n/a #
1379n/a oup = b''
1380n/a if isinstance(inp, str):
1381n/a inp = inp.encode('utf-8')
1382n/a while inp:
1383n/a if len(inp) > 48:
1384n/a t = inp[:48]
1385n/a inp = inp[48:]
1386n/a else:
1387n/a t = inp
1388n/a inp = b''
1389n/a e = binascii.b2a_base64(t)
1390n/a if e:
1391n/a oup = oup + e[:-1]
1392n/a return oup
1393n/a
1394n/a def decode(self, inp):
1395n/a if not inp:
1396n/a return b''
1397n/a return binascii.a2b_base64(inp)
1398n/a
1399n/aMonths = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')
1400n/aMon2num = {s.encode():n+1 for n, s in enumerate(Months[1:])}
1401n/a
1402n/adef Internaldate2tuple(resp):
1403n/a """Parse an IMAP4 INTERNALDATE string.
1404n/a
1405n/a Return corresponding local time. The return value is a
1406n/a time.struct_time tuple or None if the string has wrong format.
1407n/a """
1408n/a
1409n/a mo = InternalDate.match(resp)
1410n/a if not mo:
1411n/a return None
1412n/a
1413n/a mon = Mon2num[mo.group('mon')]
1414n/a zonen = mo.group('zonen')
1415n/a
1416n/a day = int(mo.group('day'))
1417n/a year = int(mo.group('year'))
1418n/a hour = int(mo.group('hour'))
1419n/a min = int(mo.group('min'))
1420n/a sec = int(mo.group('sec'))
1421n/a zoneh = int(mo.group('zoneh'))
1422n/a zonem = int(mo.group('zonem'))
1423n/a
1424n/a # INTERNALDATE timezone must be subtracted to get UT
1425n/a
1426n/a zone = (zoneh*60 + zonem)*60
1427n/a if zonen == b'-':
1428n/a zone = -zone
1429n/a
1430n/a tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1431n/a utc = calendar.timegm(tt) - zone
1432n/a
1433n/a return time.localtime(utc)
1434n/a
1435n/a
1436n/a
1437n/adef Int2AP(num):
1438n/a
1439n/a """Convert integer to A-P string representation."""
1440n/a
1441n/a val = b''; AP = b'ABCDEFGHIJKLMNOP'
1442n/a num = int(abs(num))
1443n/a while num:
1444n/a num, mod = divmod(num, 16)
1445n/a val = AP[mod:mod+1] + val
1446n/a return val
1447n/a
1448n/a
1449n/a
1450n/adef ParseFlags(resp):
1451n/a
1452n/a """Convert IMAP4 flags response to python tuple."""
1453n/a
1454n/a mo = Flags.match(resp)
1455n/a if not mo:
1456n/a return ()
1457n/a
1458n/a return tuple(mo.group('flags').split())
1459n/a
1460n/a
1461n/adef Time2Internaldate(date_time):
1462n/a
1463n/a """Convert date_time to IMAP4 INTERNALDATE representation.
1464n/a
1465n/a Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The
1466n/a date_time argument can be a number (int or float) representing
1467n/a seconds since epoch (as returned by time.time()), a 9-tuple
1468n/a representing local time, an instance of time.struct_time (as
1469n/a returned by time.localtime()), an aware datetime instance or a
1470n/a double-quoted string. In the last case, it is assumed to already
1471n/a be in the correct format.
1472n/a """
1473n/a if isinstance(date_time, (int, float)):
1474n/a dt = datetime.fromtimestamp(date_time,
1475n/a timezone.utc).astimezone()
1476n/a elif isinstance(date_time, tuple):
1477n/a try:
1478n/a gmtoff = date_time.tm_gmtoff
1479n/a except AttributeError:
1480n/a if time.daylight:
1481n/a dst = date_time[8]
1482n/a if dst == -1:
1483n/a dst = time.localtime(time.mktime(date_time))[8]
1484n/a gmtoff = -(time.timezone, time.altzone)[dst]
1485n/a else:
1486n/a gmtoff = -time.timezone
1487n/a delta = timedelta(seconds=gmtoff)
1488n/a dt = datetime(*date_time[:6], tzinfo=timezone(delta))
1489n/a elif isinstance(date_time, datetime):
1490n/a if date_time.tzinfo is None:
1491n/a raise ValueError("date_time must be aware")
1492n/a dt = date_time
1493n/a elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1494n/a return date_time # Assume in correct format
1495n/a else:
1496n/a raise ValueError("date_time not of a known type")
1497n/a fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(Months[dt.month])
1498n/a return dt.strftime(fmt)
1499n/a
1500n/a
1501n/a
1502n/aif __name__ == '__main__':
1503n/a
1504n/a # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1505n/a # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1506n/a # to test the IMAP4_stream class
1507n/a
1508n/a import getopt, getpass
1509n/a
1510n/a try:
1511n/a optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1512n/a except getopt.error as val:
1513n/a optlist, args = (), ()
1514n/a
1515n/a stream_command = None
1516n/a for opt,val in optlist:
1517n/a if opt == '-d':
1518n/a Debug = int(val)
1519n/a elif opt == '-s':
1520n/a stream_command = val
1521n/a if not args: args = (stream_command,)
1522n/a
1523n/a if not args: args = ('',)
1524n/a
1525n/a host = args[0]
1526n/a
1527n/a USER = getpass.getuser()
1528n/a PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1529n/a
1530n/a test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1531n/a test_seq1 = (
1532n/a ('login', (USER, PASSWD)),
1533n/a ('create', ('/tmp/xxx 1',)),
1534n/a ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1535n/a ('CREATE', ('/tmp/yyz 2',)),
1536n/a ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1537n/a ('list', ('/tmp', 'yy*')),
1538n/a ('select', ('/tmp/yyz 2',)),
1539n/a ('search', (None, 'SUBJECT', 'test')),
1540n/a ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1541n/a ('store', ('1', 'FLAGS', r'(\Deleted)')),
1542n/a ('namespace', ()),
1543n/a ('expunge', ()),
1544n/a ('recent', ()),
1545n/a ('close', ()),
1546n/a )
1547n/a
1548n/a test_seq2 = (
1549n/a ('select', ()),
1550n/a ('response',('UIDVALIDITY',)),
1551n/a ('uid', ('SEARCH', 'ALL')),
1552n/a ('response', ('EXISTS',)),
1553n/a ('append', (None, None, None, test_mesg)),
1554n/a ('recent', ()),
1555n/a ('logout', ()),
1556n/a )
1557n/a
1558n/a def run(cmd, args):
1559n/a M._mesg('%s %s' % (cmd, args))
1560n/a typ, dat = getattr(M, cmd)(*args)
1561n/a M._mesg('%s => %s %s' % (cmd, typ, dat))
1562n/a if typ == 'NO': raise dat[0]
1563n/a return dat
1564n/a
1565n/a try:
1566n/a if stream_command:
1567n/a M = IMAP4_stream(stream_command)
1568n/a else:
1569n/a M = IMAP4(host)
1570n/a if M.state == 'AUTH':
1571n/a test_seq1 = test_seq1[1:] # Login not needed
1572n/a M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1573n/a M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1574n/a
1575n/a for cmd,args in test_seq1:
1576n/a run(cmd, args)
1577n/a
1578n/a for ml in run('list', ('/tmp/', 'yy%')):
1579n/a mo = re.match(r'.*"([^"]+)"$', ml)
1580n/a if mo: path = mo.group(1)
1581n/a else: path = ml.split()[-1]
1582n/a run('delete', (path,))
1583n/a
1584n/a for cmd,args in test_seq2:
1585n/a dat = run(cmd, args)
1586n/a
1587n/a if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1588n/a continue
1589n/a
1590n/a uid = dat[-1].split()
1591n/a if not uid: continue
1592n/a run('uid', ('FETCH', '%s' % uid[-1],
1593n/a '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1594n/a
1595n/a print('\nAll tests OK.')
1596n/a
1597n/a except:
1598n/a print('\nTests failed.')
1599n/a
1600n/a if not Debug:
1601n/a print('''
1602n/aIf you would like to see debugging output,
1603n/atry: %s -d5
1604n/a''' % sys.argv[0])
1605n/a
1606n/a raise