Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
imaplib.py1968 linesDownload Raw Back to Lib
1"""IMAP4 client.2 3Based on RFC 2060.4 5Public class:           IMAP46Public variable:        Debug7Public functions:       Internaldate2tuple8                        Int2AP9                        ParseFlags10                        Time2Internaldate11"""12 13# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.14#15# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.16# String method conversion by ESR, February 2001.17# GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.18# IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.19# GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.20# PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.21# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.22# IDLE contributed by Forest <forestix@nom.one> August 2024.23 24__version__ = "2.60"25 26import binascii, errno, random, re, socket, subprocess, sys, time, calendar27from datetime import datetime, timezone, timedelta28from io import DEFAULT_BUFFER_SIZE29 30try:31    import ssl32    HAVE_SSL = True33except ImportError:34    HAVE_SSL = False35 36__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",37           "Int2AP", "ParseFlags", "Time2Internaldate"]38 39#       Globals40 41CRLF = b'\r\n'42Debug = 043IMAP4_PORT = 14344IMAP4_SSL_PORT = 99345AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first46 47# Maximal line length when calling readline(). This is to prevent48# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1)49# don't specify a line length. RFC 2683 suggests limiting client50# command lines to 1000 octets and that servers should be prepared51# to accept command lines up to 8000 octets, so we used to use 10K here.52# In the modern world (eg: gmail) the response to, for example, a53# search command can be quite large, so we now use 1M.54_MAXLINE = 100000055 56 57#       Commands58 59Commands = {60        # name            valid states61        'APPEND':       ('AUTH', 'SELECTED'),62        'AUTHENTICATE': ('NONAUTH',),63        'CAPABILITY':   ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),64        'CHECK':        ('SELECTED',),65        'CLOSE':        ('SELECTED',),66        'COPY':         ('SELECTED',),67        'CREATE':       ('AUTH', 'SELECTED'),68        'DELETE':       ('AUTH', 'SELECTED'),69        'DELETEACL':    ('AUTH', 'SELECTED'),70        'ENABLE':       ('AUTH', ),71        'EXAMINE':      ('AUTH', 'SELECTED'),72        'EXPUNGE':      ('SELECTED',),73        'FETCH':        ('SELECTED',),74        'GETACL':       ('AUTH', 'SELECTED'),75        'GETANNOTATION':('AUTH', 'SELECTED'),76        'GETQUOTA':     ('AUTH', 'SELECTED'),77        'GETQUOTAROOT': ('AUTH', 'SELECTED'),78        'IDLE':         ('AUTH', 'SELECTED'),79        'MYRIGHTS':     ('AUTH', 'SELECTED'),80        'LIST':         ('AUTH', 'SELECTED'),81        'LOGIN':        ('NONAUTH',),82        'LOGOUT':       ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),83        'LSUB':         ('AUTH', 'SELECTED'),84        'MOVE':         ('SELECTED',),85        'NAMESPACE':    ('AUTH', 'SELECTED'),86        'NOOP':         ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),87        'PARTIAL':      ('SELECTED',),                                  # NB: obsolete88        'PROXYAUTH':    ('AUTH',),89        'RENAME':       ('AUTH', 'SELECTED'),90        'SEARCH':       ('SELECTED',),91        'SELECT':       ('AUTH', 'SELECTED'),92        'SETACL':       ('AUTH', 'SELECTED'),93        'SETANNOTATION':('AUTH', 'SELECTED'),94        'SETQUOTA':     ('AUTH', 'SELECTED'),95        'SORT':         ('SELECTED',),96        'STARTTLS':     ('NONAUTH',),97        'STATUS':       ('AUTH', 'SELECTED'),98        'STORE':        ('SELECTED',),99        'SUBSCRIBE':    ('AUTH', 'SELECTED'),100        'THREAD':       ('SELECTED',),101        'UID':          ('SELECTED',),102        'UNSUBSCRIBE':  ('AUTH', 'SELECTED'),103        'UNSELECT':     ('SELECTED',),104        }105 106#       Patterns to match server responses107 108Continuation = re.compile(br'\+( (?P<data>.*))?')109Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')110InternalDate = re.compile(br'.*INTERNALDATE "'111        br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'112        br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'113        br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'114        br'"')115# Literal is no longer used; kept for backward compatibility.116Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)117MapCRLF = re.compile(br'\r\n|\r|\n')118# We no longer exclude the ']' character from the data portion of the response119# code, even though it violates the RFC.  Popular IMAP servers such as Gmail120# allow flags with ']', and there are programs (including imaplib!) that can121# produce them.  The problem with this is if the 'text' portion of the response122# includes a ']' we'll parse the response wrong (which is the point of the RFC123# restriction).  However, that seems less likely to be a problem in practice124# than being unable to correctly parse flags that include ']' chars, which125# was reported as a real-world problem in issue #21815.126Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>.*))?\]')127Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')128# Untagged_status is no longer used; kept for backward compatibility129Untagged_status = re.compile(130    br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)131# We compile these in _mode_xxx.132_Literal = br'.*{(?P<size>\d+)}$'133_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'134 135 136 137class IMAP4:138 139    r"""IMAP4 client class.140 141    Instantiate with: IMAP4([host[, port[, timeout=None]]])142 143            host - host's name (default: localhost);144            port - port number (default: standard IMAP4 port).145            timeout - socket timeout (default: None)146                      If timeout is not given or is None,147                      the global default socket timeout is used148 149    All IMAP4rev1 commands are supported by methods of the same150    name (in lowercase).151 152    All arguments to commands are converted to strings, except for153    AUTHENTICATE, and the last argument to APPEND which is passed as154    an IMAP4 literal.  If necessary (the string contains any155    non-printing characters or white-space and isn't enclosed with156    either parentheses or double quotes) each string is quoted.157    However, the 'password' argument to the LOGIN command is always158    quoted.  If you want to avoid having an argument string quoted159    (eg: the 'flags' argument to STORE) then enclose the string in160    parentheses (eg: "(\Deleted)").161 162    Each command returns a tuple: (type, [data, ...]) where 'type'163    is usually 'OK' or 'NO', and 'data' is either the text from the164    tagged response, or untagged results from command. Each 'data'165    is either a string, or a tuple. If a tuple, then the first part166    is the header of the response, and the second part contains167    the data (ie: 'literal' value).168 169    Errors raise the exception class <instance>.error("<reason>").170    IMAP4 server errors raise <instance>.abort("<reason>"),171    which is a sub-class of 'error'. Mailbox status changes172    from READ-WRITE to READ-ONLY raise the exception class173    <instance>.readonly("<reason>"), which is a sub-class of 'abort'.174 175    "error" exceptions imply a program error.176    "abort" exceptions imply the connection should be reset, and177            the command re-tried.178    "readonly" exceptions imply the command should be re-tried.179 180    Note: to use this module, you must read the RFCs pertaining to the181    IMAP4 protocol, as the semantics of the arguments to each IMAP4182    command are left to the invoker, not to mention the results. Also,183    most IMAP servers implement a sub-set of the commands available here.184    """185 186    class error(Exception): pass    # Logical errors - debug required187    class abort(error): pass        # Service errors - close and retry188    class readonly(abort): pass     # Mailbox status changed to READ-ONLY189    class _responsetimeout(TimeoutError): pass # No response during IDLE190 191    def __init__(self, host='', port=IMAP4_PORT, timeout=None):192        self.debug = Debug193        self.state = 'LOGOUT'194        self.literal = None             # A literal argument to a command195        self.tagged_commands = {}       # Tagged commands awaiting response196        self.untagged_responses = {}    # {typ: [data, ...], ...}197        self.continuation_response = '' # Last continuation response198        self._idle_responses = []       # Response queue for idle iteration199        self._idle_capture = False      # Whether to queue responses for idle200        self.is_readonly = False        # READ-ONLY desired state201        self.tagnum = 0202        self._tls_established = False203        self._mode_ascii()204        self._readbuf = []205 206        # Open socket to server.207 208        self.open(host, port, timeout)209 210        try:211            self._connect()212        except Exception:213            try:214                self.shutdown()215            except OSError:216                pass217            raise218 219    def _mode_ascii(self):220        self.utf8_enabled = False221        self._encoding = 'ascii'222        self.Literal = re.compile(_Literal, re.ASCII)223        self.Untagged_status = re.compile(_Untagged_status, re.ASCII)224 225 226    def _mode_utf8(self):227        self.utf8_enabled = True228        self._encoding = 'utf-8'229        self.Literal = re.compile(_Literal)230        self.Untagged_status = re.compile(_Untagged_status)231 232 233    def _connect(self):234        # Create unique tag for this session,235        # and compile tagged response matcher.236 237        self.tagpre = Int2AP(random.randint(4096, 65535))238        self.tagre = re.compile(br'(?P<tag>'239                        + self.tagpre240                        + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)241 242        # Get server welcome message,243        # request and store CAPABILITY response.244 245        if __debug__:246            self._cmd_log_len = 10247            self._cmd_log_idx = 0248            self._cmd_log = {}           # Last '_cmd_log_len' interactions249            if self.debug >= 1:250                self._mesg('imaplib version %s' % __version__)251                self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)252 253        self.welcome = self._get_response()254        if 'PREAUTH' in self.untagged_responses:255            self.state = 'AUTH'256        elif 'OK' in self.untagged_responses:257            self.state = 'NONAUTH'258        else:259            raise self.error(self.welcome)260 261        self._get_capabilities()262        if __debug__:263            if self.debug >= 3:264                self._mesg('CAPABILITIES: %r' % (self.capabilities,))265 266        for version in AllowedVersions:267            if not version in self.capabilities:268                continue269            self.PROTOCOL_VERSION = version270            return271 272        raise self.error('server not IMAP4 compliant')273 274 275    def __getattr__(self, attr):276        #       Allow UPPERCASE variants of IMAP4 command methods.277        if attr in Commands:278            return getattr(self, attr.lower())279        raise AttributeError("Unknown IMAP4 command: '%s'" % attr)280 281    def __enter__(self):282        return self283 284    def __exit__(self, *args):285        if self.state == "LOGOUT":286            return287 288        try:289            self.logout()290        except OSError:291            pass292 293 294    #       Overridable methods295 296 297    def _create_socket(self, timeout):298        # Default value of IMAP4.host is '', but socket.getaddrinfo()299        # (which is used by socket.create_connection()) expects None300        # as a default value for host.301        if timeout is not None and not timeout:302            raise ValueError('Non-blocking socket (timeout=0) is not supported')303        host = None if not self.host else self.host304        sys.audit("imaplib.open", self, self.host, self.port)305        address = (host, self.port)306        if timeout is not None:307            return socket.create_connection(address, timeout)308        return socket.create_connection(address)309 310    def open(self, host='', port=IMAP4_PORT, timeout=None):311        """Setup connection to remote server on "host:port"312            (default: localhost:standard IMAP4 port).313        This connection will be used by the routines:314            read, readline, send, shutdown.315        """316        self.host = host317        self.port = port318        self.sock = self._create_socket(timeout)319        self._file = self.sock.makefile('rb')320 321 322    @property323    def file(self):324        # The old 'file' attribute is no longer used now that we do our own325        # read() and readline() buffering, with which it conflicts.326        # As an undocumented interface, it should never have been accessed by327        # external code, and therefore does not warrant deprecation.328        # Nevertheless, we provide this property for now, to avoid suddenly329        # breaking any code in the wild that might have been using it in a330        # harmless way.331        import warnings332        warnings.warn(333            'IMAP4.file is unsupported, can cause errors, and may be removed.',334            RuntimeWarning,335            stacklevel=2)336        return self._file337 338 339    def read(self, size):340        """Read 'size' bytes from remote."""341        # We need buffered read() to continue working after socket timeouts,342        # since we use them during IDLE. Unfortunately, the standard library's343        # SocketIO implementation makes this impossible, by setting a permanent344        # error condition instead of letting the caller decide how to handle a345        # timeout. We therefore implement our own buffered read().346        # https://github.com/python/cpython/issues/51571347        #348        # Reading in chunks instead of delegating to a single349        # BufferedReader.read() call also means we avoid its preallocation350        # of an unreasonably large memory block if a malicious server claims351        # it will send a huge literal without actually sending one.352        # https://github.com/python/cpython/issues/119511353 354        parts = []355 356        while size > 0:357 358            if len(parts) < len(self._readbuf):359                buf = self._readbuf[len(parts)]360            else:361                try:362                    buf = self.sock.recv(DEFAULT_BUFFER_SIZE)363                except ConnectionError:364                    break365                if not buf:366                    break367                self._readbuf.append(buf)368 369            if len(buf) >= size:370                parts.append(buf[:size])371                self._readbuf = [buf[size:]] + self._readbuf[len(parts):]372                break373            parts.append(buf)374            size -= len(buf)375 376        return b''.join(parts)377 378 379    def readline(self):380        """Read line from remote."""381        # The comment in read() explains why we implement our own readline().382 383        LF = b'\n'384        parts = []385        length = 0386 387        while length < _MAXLINE:388 389            if len(parts) < len(self._readbuf):390                buf = self._readbuf[len(parts)]391            else:392                try:393                    buf = self.sock.recv(DEFAULT_BUFFER_SIZE)394                except ConnectionError:395                    break396                if not buf:397                    break398                self._readbuf.append(buf)399 400            pos = buf.find(LF)401            if pos != -1:402                pos += 1403                parts.append(buf[:pos])404                self._readbuf = [buf[pos:]] + self._readbuf[len(parts):]405                break406            parts.append(buf)407            length += len(buf)408 409        line = b''.join(parts)410        if len(line) > _MAXLINE:411            raise self.error("got more than %d bytes" % _MAXLINE)412        return line413 414 415    def send(self, data):416        """Send data to remote."""417        sys.audit("imaplib.send", self, data)418        self.sock.sendall(data)419 420 421    def shutdown(self):422        """Close I/O established in "open"."""423        self._file.close()424        try:425            self.sock.shutdown(socket.SHUT_RDWR)426        except OSError as exc:427            # The server might already have closed the connection.428            # On Windows, this may result in WSAEINVAL (error 10022):429            # An invalid operation was attempted.430            if (exc.errno != errno.ENOTCONN431               and getattr(exc, 'winerror', 0) != 10022):432                raise433        finally:434            self.sock.close()435 436 437    def socket(self):438        """Return socket instance used to connect to IMAP4 server.439 440        socket = <instance>.socket()441        """442        return self.sock443 444 445 446    #       Utility methods447 448 449    def recent(self):450        """Return most recent 'RECENT' responses if any exist,451        else prompt server for an update using the 'NOOP' command.452 453        (typ, [data]) = <instance>.recent()454 455        'data' is None if no new messages,456        else list of RECENT responses, most recent last.457        """458        name = 'RECENT'459        typ, dat = self._untagged_response('OK', [None], name)460        if dat[-1]:461            return typ, dat462        typ, dat = self.noop()  # Prod server for response463        return self._untagged_response(typ, dat, name)464 465 466    def response(self, code):467        """Return data for response 'code' if received, or None.468 469        Old value for response 'code' is cleared.470 471        (code, [data]) = <instance>.response(code)472        """473        return self._untagged_response(code, [None], code.upper())474 475 476 477    #       IMAP4 commands478 479 480    def append(self, mailbox, flags, date_time, message):481        """Append message to named mailbox.482 483        (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)484 485                All args except 'message' can be None.486        """487        name = 'APPEND'488        if not mailbox:489            mailbox = 'INBOX'490        if flags:491            if (flags[0],flags[-1]) != ('(',')'):492                flags = '(%s)' % flags493        else:494            flags = None495        if date_time:496            date_time = Time2Internaldate(date_time)497        else:498            date_time = None499        literal = MapCRLF.sub(CRLF, message)500        self.literal = literal501        return self._simple_command(name, mailbox, flags, date_time)502 503 504    def authenticate(self, mechanism, authobject):505        """Authenticate command - requires response processing.506 507        'mechanism' specifies which authentication mechanism is to508        be used - it must appear in <instance>.capabilities in the509        form AUTH=<mechanism>.510 511        'authobject' must be a callable object:512 513                data = authobject(response)514 515        It will be called to process server continuation responses; the516        response argument it is passed will be a bytes.  It should return bytes517        data that will be base64 encoded and sent to the server.  It should518        return None if the client abort response '*' should be sent instead.519        """520        mech = mechanism.upper()521        # XXX: shouldn't this code be removed, not commented out?522        #cap = 'AUTH=%s' % mech523        #if not cap in self.capabilities:       # Let the server decide!524        #    raise self.error("Server doesn't allow %s authentication." % mech)525        self.literal = _Authenticator(authobject).process526        typ, dat = self._simple_command('AUTHENTICATE', mech)527        if typ != 'OK':528            raise self.error(dat[-1].decode('utf-8', 'replace'))529        self.state = 'AUTH'530        return typ, dat531 532 533    def capability(self):534        """(typ, [data]) = <instance>.capability()535        Fetch capabilities list from server."""536 537        name = 'CAPABILITY'538        typ, dat = self._simple_command(name)539        return self._untagged_response(typ, dat, name)540 541 542    def check(self):543        """Checkpoint mailbox on server.544 545        (typ, [data]) = <instance>.check()546        """547        return self._simple_command('CHECK')548 549 550    def close(self):551        """Close currently selected mailbox.552 553        Deleted messages are removed from writable mailbox.554        This is the recommended command before 'LOGOUT'.555 556        (typ, [data]) = <instance>.close()557        """558        try:559            typ, dat = self._simple_command('CLOSE')560        finally:561            self.state = 'AUTH'562        return typ, dat563 564 565    def copy(self, message_set, new_mailbox):566        """Copy 'message_set' messages onto end of 'new_mailbox'.567 568        (typ, [data]) = <instance>.copy(message_set, new_mailbox)569        """570        return self._simple_command('COPY', message_set, new_mailbox)571 572 573    def create(self, mailbox):574        """Create new mailbox.575 576        (typ, [data]) = <instance>.create(mailbox)577        """578        return self._simple_command('CREATE', mailbox)579 580 581    def delete(self, mailbox):582        """Delete old mailbox.583 584        (typ, [data]) = <instance>.delete(mailbox)585        """586        return self._simple_command('DELETE', mailbox)587 588    def deleteacl(self, mailbox, who):589        """Delete the ACLs (remove any rights) set for who on mailbox.590 591        (typ, [data]) = <instance>.deleteacl(mailbox, who)592        """593        return self._simple_command('DELETEACL', mailbox, who)594 595    def enable(self, capability):596        """Send an RFC5161 enable string to the server.597 598        (typ, [data]) = <instance>.enable(capability)599        """600        if 'ENABLE' not in self.capabilities:601            raise IMAP4.error("Server does not support ENABLE")602        typ, data = self._simple_command('ENABLE', capability)603        if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():604            self._mode_utf8()605        return typ, data606 607    def expunge(self):608        """Permanently remove deleted items from selected mailbox.609 610        Generates 'EXPUNGE' response for each deleted message.611 612        (typ, [data]) = <instance>.expunge()613 614        'data' is list of 'EXPUNGE'd message numbers in order received.615        """616        name = 'EXPUNGE'617        typ, dat = self._simple_command(name)618        return self._untagged_response(typ, dat, name)619 620 621    def fetch(self, message_set, message_parts):622        """Fetch (parts of) messages.623 624        (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)625 626        'message_parts' should be a string of selected parts627        enclosed in parentheses, eg: "(UID BODY[TEXT])".628 629        'data' are tuples of message part envelope and data.630        """631        name = 'FETCH'632        typ, dat = self._simple_command(name, message_set, message_parts)633        return self._untagged_response(typ, dat, name)634 635 636    def getacl(self, mailbox):637        """Get the ACLs for a mailbox.638 639        (typ, [data]) = <instance>.getacl(mailbox)640        """641        typ, dat = self._simple_command('GETACL', mailbox)642        return self._untagged_response(typ, dat, 'ACL')643 644 645    def getannotation(self, mailbox, entry, attribute):646        """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)647        Retrieve ANNOTATIONs."""648 649        typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)650        return self._untagged_response(typ, dat, 'ANNOTATION')651 652 653    def getquota(self, root):654        """Get the quota root's resource usage and limits.655 656        Part of the IMAP4 QUOTA extension defined in rfc2087.657 658        (typ, [data]) = <instance>.getquota(root)659        """660        typ, dat = self._simple_command('GETQUOTA', root)661        return self._untagged_response(typ, dat, 'QUOTA')662 663 664    def getquotaroot(self, mailbox):665        """Get the list of quota roots for the named mailbox.666 667        (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)668        """669        typ, dat = self._simple_command('GETQUOTAROOT', mailbox)670        typ, quota = self._untagged_response(typ, dat, 'QUOTA')671        typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')672        return typ, [quotaroot, quota]673 674 675    def idle(self, duration=None):676        """Return an iterable IDLE context manager producing untagged responses.677        If the argument is not None, limit iteration to 'duration' seconds.678 679        with M.idle(duration=29 * 60) as idler:680            for typ, data in idler:681                print(typ, data)682 683        Note: 'duration' requires a socket connection (not IMAP4_stream).684        """685        return Idler(self, duration)686 687 688    def list(self, directory='""', pattern='*'):689        """List mailbox names in directory matching pattern.690 691        (typ, [data]) = <instance>.list(directory='""', pattern='*')692 693        'data' is list of LIST responses.694        """695        name = 'LIST'696        typ, dat = self._simple_command(name, directory, pattern)697        return self._untagged_response(typ, dat, name)698 699 700    def login(self, user, password):701        """Identify client using plaintext password.702 703        (typ, [data]) = <instance>.login(user, password)704 705        NB: 'password' will be quoted.706        """707        typ, dat = self._simple_command('LOGIN', user, self._quote(password))708        if typ != 'OK':709            raise self.error(dat[-1])710        self.state = 'AUTH'711        return typ, dat712 713 714    def login_cram_md5(self, user, password):715        """ Force use of CRAM-MD5 authentication.716 717        (typ, [data]) = <instance>.login_cram_md5(user, password)718        """719        self.user, self.password = user, password720        return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)721 722 723    def _CRAM_MD5_AUTH(self, challenge):724        """ Authobject to use with CRAM-MD5 authentication. """725        import hmac726 727        if isinstance(self.password, str):728            password = self.password.encode('utf-8')729        else:730            password = self.password731 732        try:733            authcode = hmac.HMAC(password, challenge, 'md5')734        except ValueError:  # HMAC-MD5 is not available735            raise self.error("CRAM-MD5 authentication is not supported")736        return f"{self.user} {authcode.hexdigest()}"737 738 739    def logout(self):740        """Shutdown connection to server.741 742        (typ, [data]) = <instance>.logout()743 744        Returns server 'BYE' response.745        """746        self.state = 'LOGOUT'747        typ, dat = self._simple_command('LOGOUT')748        self.shutdown()749        return typ, dat750 751 752    def lsub(self, directory='""', pattern='*'):753        """List 'subscribed' mailbox names in directory matching pattern.754 755        (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')756 757        'data' are tuples of message part envelope and data.758        """759        name = 'LSUB'760        typ, dat = self._simple_command(name, directory, pattern)761        return self._untagged_response(typ, dat, name)762 763    def myrights(self, mailbox):764        """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).765 766        (typ, [data]) = <instance>.myrights(mailbox)767        """768        typ,dat = self._simple_command('MYRIGHTS', mailbox)769        return self._untagged_response(typ, dat, 'MYRIGHTS')770 771    def namespace(self):772        """ Returns IMAP namespaces ala rfc2342773 774        (typ, [data, ...]) = <instance>.namespace()775        """776        name = 'NAMESPACE'777        typ, dat = self._simple_command(name)778        return self._untagged_response(typ, dat, name)779 780 781    def noop(self):782        """Send NOOP command.783 784        (typ, [data]) = <instance>.noop()785        """786        if __debug__:787            if self.debug >= 3:788                self._dump_ur(self.untagged_responses)789        return self._simple_command('NOOP')790 791 792    def partial(self, message_num, message_part, start, length):793        """Fetch truncated part of a message.794 795        (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)796 797        'data' is tuple of message part envelope and data.798        """799        name = 'PARTIAL'800        typ, dat = self._simple_command(name, message_num, message_part, start, length)801        return self._untagged_response(typ, dat, 'FETCH')802 803 804    def proxyauth(self, user):805        """Assume authentication as "user".806 807        Allows an authorised administrator to proxy into any user's808        mailbox.809 810        (typ, [data]) = <instance>.proxyauth(user)811        """812 813        name = 'PROXYAUTH'814        return self._simple_command('PROXYAUTH', user)815 816 817    def rename(self, oldmailbox, newmailbox):818        """Rename old mailbox name to new.819 820        (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)821        """822        return self._simple_command('RENAME', oldmailbox, newmailbox)823 824 825    def search(self, charset, *criteria):826        """Search mailbox for matching messages.827 828        (typ, [data]) = <instance>.search(charset, criterion, ...)829 830        'data' is space separated list of matching message numbers.831        If UTF8 is enabled, charset MUST be None.832        """833        name = 'SEARCH'834        if charset:835            if self.utf8_enabled:836                raise IMAP4.error("Non-None charset not valid in UTF8 mode")837            typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)838        else:839            typ, dat = self._simple_command(name, *criteria)840        return self._untagged_response(typ, dat, name)841 842 843    def select(self, mailbox='INBOX', readonly=False):844        """Select a mailbox.845 846        Flush all untagged responses.847 848        (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)849 850        'data' is count of messages in mailbox ('EXISTS' response).851 852        Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so853        other responses should be obtained via <instance>.response('FLAGS') etc.854        """855        self.untagged_responses = {}    # Flush old responses.856        self.is_readonly = readonly857        if readonly:858            name = 'EXAMINE'859        else:860            name = 'SELECT'861        typ, dat = self._simple_command(name, mailbox)862        if typ != 'OK':863            self.state = 'AUTH'     # Might have been 'SELECTED'864            return typ, dat865        self.state = 'SELECTED'866        if 'READ-ONLY' in self.untagged_responses \867                and not readonly:868            if __debug__:869                if self.debug >= 1:870                    self._dump_ur(self.untagged_responses)871            raise self.readonly('%s is not writable' % mailbox)872        return typ, self.untagged_responses.get('EXISTS', [None])873 874 875    def setacl(self, mailbox, who, what):876        """Set a mailbox acl.877 878        (typ, [data]) = <instance>.setacl(mailbox, who, what)879        """880        return self._simple_command('SETACL', mailbox, who, what)881 882 883    def setannotation(self, *args):884        """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)885        Set ANNOTATIONs."""886 887        typ, dat = self._simple_command('SETANNOTATION', *args)888        return self._untagged_response(typ, dat, 'ANNOTATION')889 890 891    def setquota(self, root, limits):892        """Set the quota root's resource limits.893 894        (typ, [data]) = <instance>.setquota(root, limits)895        """896        typ, dat = self._simple_command('SETQUOTA', root, limits)897        return self._untagged_response(typ, dat, 'QUOTA')898 899 900    def sort(self, sort_criteria, charset, *search_criteria):901        """IMAP4rev1 extension SORT command.902 903        (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)904        """905        name = 'SORT'906        #if not name in self.capabilities:      # Let the server decide!907        #       raise self.error('unimplemented extension command: %s' % name)908        if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):909            sort_criteria = '(%s)' % sort_criteria910        typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)911        return self._untagged_response(typ, dat, name)912 913 914    def starttls(self, ssl_context=None):915        name = 'STARTTLS'916        if not HAVE_SSL:917            raise self.error('SSL support missing')918        if self._tls_established:919            raise self.abort('TLS session already established')920        if name not in self.capabilities:921            raise self.abort('TLS not supported by server')922        # Generate a default SSL context if none was passed.923        if ssl_context is None:924            ssl_context = ssl._create_stdlib_context()925        typ, dat = self._simple_command(name)926        if typ == 'OK':927            self.sock = ssl_context.wrap_socket(self.sock,928                                                server_hostname=self.host)929            self._file = self.sock.makefile('rb')930            self._tls_established = True931            self._get_capabilities()932        else:933            raise self.error("Couldn't establish TLS session")934        return self._untagged_response(typ, dat, name)935 936 937    def status(self, mailbox, names):938        """Request named status conditions for mailbox.939 940        (typ, [data]) = <instance>.status(mailbox, names)941        """942        name = 'STATUS'943        #if self.PROTOCOL_VERSION == 'IMAP4':   # Let the server decide!944        #    raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)945        typ, dat = self._simple_command(name, mailbox, names)946        return self._untagged_response(typ, dat, name)947 948 949    def store(self, message_set, command, flags):950        """Alters flag dispositions for messages in mailbox.951 952        (typ, [data]) = <instance>.store(message_set, command, flags)953        """954        if (flags[0],flags[-1]) != ('(',')'):955            flags = '(%s)' % flags  # Avoid quoting the flags956        typ, dat = self._simple_command('STORE', message_set, command, flags)957        return self._untagged_response(typ, dat, 'FETCH')958 959 960    def subscribe(self, mailbox):961        """Subscribe to new mailbox.962 963        (typ, [data]) = <instance>.subscribe(mailbox)964        """965        return self._simple_command('SUBSCRIBE', mailbox)966 967 968    def thread(self, threading_algorithm, charset, *search_criteria):969        """IMAPrev1 extension THREAD command.970 971        (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)972        """973        name = 'THREAD'974        typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)975        return self._untagged_response(typ, dat, name)976 977 978    def uid(self, command, *args):979        """Execute "command arg ..." with messages identified by UID,980                rather than message number.981 982        (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)983 984        Returns response appropriate to 'command'.985        """986        command = command.upper()987        if not command in Commands:988            raise self.error("Unknown IMAP4 UID command: %s" % command)989        if self.state not in Commands[command]:990            raise self.error("command %s illegal in state %s, "991                             "only allowed in states %s" %992                             (command, self.state,993                              ', '.join(Commands[command])))994        name = 'UID'995        typ, dat = self._simple_command(name, command, *args)996        if command in ('SEARCH', 'SORT', 'THREAD'):997            name = command998        else:999            name = 'FETCH'1000        return self._untagged_response(typ, dat, name)1001 1002 1003    def unsubscribe(self, mailbox):1004        """Unsubscribe from old mailbox.1005 1006        (typ, [data]) = <instance>.unsubscribe(mailbox)1007        """1008        return self._simple_command('UNSUBSCRIBE', mailbox)1009 1010 1011    def unselect(self):1012        """Free server's resources associated with the selected mailbox1013        and returns the server to the authenticated state.1014        This command performs the same actions as CLOSE, except1015        that no messages are permanently removed from the currently1016        selected mailbox.1017 1018        (typ, [data]) = <instance>.unselect()1019        """1020        try:1021            typ, data = self._simple_command('UNSELECT')1022        finally:1023            self.state = 'AUTH'1024        return typ, data1025 1026 1027    def xatom(self, name, *args):1028        """Allow simple extension commands1029                notified by server in CAPABILITY response.1030 1031        Assumes command is legal in current state.1032 1033        (typ, [data]) = <instance>.xatom(name, arg, ...)1034 1035        Returns response appropriate to extension command 'name'.1036        """1037        name = name.upper()1038        #if not name in self.capabilities:      # Let the server decide!1039        #    raise self.error('unknown extension command: %s' % name)1040        if not name in Commands:1041            Commands[name] = (self.state,)1042        return self._simple_command(name, *args)1043 1044 1045 1046    #       Private methods1047 1048 1049    def _append_untagged(self, typ, dat):1050        if dat is None:1051            dat = b''1052 1053        # During idle, queue untagged responses for delivery via iteration1054        if self._idle_capture:1055            # Responses containing literal strings are passed to us one data1056            # fragment at a time, while others arrive in a single call.1057            if (not self._idle_responses or1058                isinstance(self._idle_responses[-1][1][-1], bytes)):1059                # We are not continuing a fragmented response; start a new one1060                self._idle_responses.append((typ, [dat]))1061            else:1062                # We are continuing a fragmented response; append the fragment1063                response = self._idle_responses[-1]1064                assert response[0] == typ1065                response[1].append(dat)1066            if __debug__ and self.debug >= 5:1067                self._mesg(f'idle: queue untagged {typ} {dat!r}')1068            return1069 1070        ur = self.untagged_responses1071        if __debug__:1072            if self.debug >= 5:1073                self._mesg('untagged_responses[%s] %s += ["%r"]' %1074                        (typ, len(ur.get(typ,'')), dat))1075        if typ in ur:1076            ur[typ].append(dat)1077        else:1078            ur[typ] = [dat]1079 1080 1081    def _check_bye(self):1082        bye = self.untagged_responses.get('BYE')1083        if bye:1084            raise self.abort(bye[-1].decode(self._encoding, 'replace'))1085 1086 1087    def _command(self, name, *args):1088 1089        if self.state not in Commands[name]:1090            self.literal = None1091            raise self.error("command %s illegal in state %s, "1092                             "only allowed in states %s" %1093                             (name, self.state,1094                              ', '.join(Commands[name])))1095 1096        for typ in ('OK', 'NO', 'BAD'):1097            if typ in self.untagged_responses:1098                del self.untagged_responses[typ]1099 1100        if 'READ-ONLY' in self.untagged_responses \1101        and not self.is_readonly:1102            raise self.readonly('mailbox status changed to READ-ONLY')1103 1104        tag = self._new_tag()1105        name = bytes(name, self._encoding)1106        data = tag + b' ' + name1107        for arg in args:1108            if arg is None: continue1109            if isinstance(arg, str):1110                arg = bytes(arg, self._encoding)1111            data = data + b' ' + arg1112 1113        literal = self.literal1114        if literal is not None:1115            self.literal = None1116            if type(literal) is type(self._command):1117                literator = literal1118            else:1119                literator = None1120                if self.utf8_enabled:1121                    data = data + bytes(' UTF8 (~{%s}' % len(literal), self._encoding)1122                    literal = literal + b')'1123                else:1124                    data = data + bytes(' {%s}' % len(literal), self._encoding)1125 1126        if __debug__:1127            if self.debug >= 4:1128                self._mesg('> %r' % data)1129            else:1130                self._log('> %r' % data)1131 1132        try:1133            self.send(data + CRLF)1134        except OSError as val:1135            raise self.abort('socket error: %s' % val)1136 1137        if literal is None:1138            return tag1139 1140        while 1:1141            # Wait for continuation response1142 1143            while self._get_response():1144                if self.tagged_commands[tag]:   # BAD/NO?1145                    return tag1146 1147            # Send literal1148 1149            if literator:1150                literal = literator(self.continuation_response)1151 1152            if __debug__:1153                if self.debug >= 4:1154                    self._mesg('write literal size %s' % len(literal))1155 1156            try:1157                self.send(literal)1158                self.send(CRLF)1159            except OSError as val:1160                raise self.abort('socket error: %s' % val)1161 1162            if not literator:1163                break1164 1165        return tag1166 1167 1168    def _command_complete(self, name, tag):1169        logout = (name == 'LOGOUT')1170        # BYE is expected after LOGOUT1171        if not logout:1172            self._check_bye()1173        try:1174            typ, data = self._get_tagged_response(tag, expect_bye=logout)1175        except self.abort as val:1176            raise self.abort('command: %s => %s' % (name, val))1177        except self.error as val:1178            raise self.error('command: %s => %s' % (name, val))1179        if not logout:1180            self._check_bye()1181        if typ == 'BAD':1182            raise self.error('%s command error: %s %s' % (name, typ, data))1183        return typ, data1184 1185 1186    def _get_capabilities(self):1187        typ, dat = self.capability()1188        if dat == [None]:1189            raise self.error('no CAPABILITY response from server')1190        dat = str(dat[-1], self._encoding)1191        dat = dat.upper()1192        self.capabilities = tuple(dat.split())1193 1194 1195    def _get_response(self, start_timeout=False):1196 1197        # Read response and store.1198        #1199        # Returns None for continuation responses,1200        # otherwise first response line received.

Showing the first 1,200 of 1968 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai