├── .gitignore ├── COPYRIGHT ├── README ├── README.changelog ├── example.conf ├── imap2maildir.py ├── pydoc ├── imap2maildir.html ├── simpleimap.html └── testsuite.html ├── rfc822py3.py ├── shuffle_by_year.py ├── simpleimap.py └── testsuite.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Some reasonable default files to ignore. 2 | # .pyc and .pyo: Python stuff 3 | *.pyc 4 | *.pyo 5 | # Tilde files: backups from editors 6 | *~ 7 | # *.conf: configuration files, except example.conf 8 | *.conf 9 | !example.conf 10 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2011 Ryan S. Tucker 2 | Copyright (c) 2011 Mark J. Nenadov 3 | Copyright (c) 2009 Timothy J Fontaine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Usage: imap2maildir [options] 2 | 3 | A script to copy a remote IMAP folder to a local mail storage area. Ideal for 4 | incremental backups of mail from free webmail providers, or perhaps as an 5 | alternative to fetchmail. Supports mbox and maildir, despite the name. See 6 | COPYRIGHT for your rights; https://github.com/rtucker/imap2maildir/ for info. 7 | 8 | Options: 9 | --version show program's version number and exit 10 | -h, --help show this help message and exit 11 | 12 | Required options: 13 | -u USERNAME, --username=USERNAME 14 | Username for authentication to IMAP server 15 | -d PATH, --destination=PATH 16 | Where to store the mail, e.g. ~/Backups/Gmail 17 | 18 | Optional and debugging options: 19 | -p PASSWORD, --password=PASSWORD 20 | Password for IMAP server. Default: prompt user 21 | -H HOSTNAME, --hostname=HOSTNAME 22 | Hostname of IMAP server, default: imap.gmail.com 23 | -P PORT, --port=PORT 24 | Port number. Default: 993 (SSL), 143 (clear) 25 | -v, --verbose Turns up the verbosity 26 | -q, --quiet Quiets all output (except prompts and errors) 27 | -r FOLDERNAME, --remote-folder=FOLDERNAME 28 | Remote IMAP folder. Default: [Gmail]/All Mail 29 | -s CRITERIA, --search=CRITERIA 30 | IMAP4 search criteria to use. Default: SEEN 31 | --create If --destination doesn't exist, create it 32 | -T, --no-turbo Check for message locally before asking IMAP. 33 | Default: True 34 | -m MAX, --max-messages=MAX 35 | How many messages to process in one run (0=infinite). 36 | Default: 0 37 | -c CONFIGFILE, --config-file=CONFIGFILE 38 | Configuration file to use. Default: imap2maildir.conf 39 | -S, --ssl Use SSL to connect, default: True 40 | -t TYPE, --type=TYPE 41 | Mailbox type. Choice of: maildir, mbox. Default: 42 | maildir 43 | --mboxdash Use - in the mbox From line instead of sender's 44 | address. Default: False 45 | 46 | COMMAND LINE EXAMPLES 47 | $ imap2maildir -u bob@yourplace.com -d /home/bob/backups/mail --create 48 | Prompts you for bob@yourplace.com's password, uses the default mail server. 49 | $ imap2maildir -c blarf.conf 50 | Uses the configuration from blarf.conf 51 | $ imap2maildir -c something.conf -m 5000 52 | Uses the something.conf configuration, but overrides maxmessages to 5000. 53 | 54 | COMMON ISSUES 55 | 1. Google Mail users in the United Kingdom receive the following: 56 | Exception: ['[NONEXISTENT] Unknown Mailbox: [Gmail]/All Mail (Failure)'] 57 | 58 | Workaround: Add --remote-folder="[Google Mail]/All Mail" 59 | Why: http://en.wikipedia.org/wiki/Gmail#Trademark_disputes 60 | 61 | 2. When a message is deleted from the server, it isn't deleted from 62 | imap2maildir's local store. 63 | 64 | My response, to someone who asked about this: 65 | It's pretty much intentional. The original intent of the script was 66 | to automatically back up mail stored on a server, so it doesn't check 67 | for deleted messages -- if a message is missing from the server, an 68 | unattended delete of the local copy would be bad! 69 | 70 | There are certainly scenarios where local deletes would be good. In 71 | fact, yours is probably one of them. :-) At first glance, it would 72 | involve iterating through all local messages and asking the server if 73 | they exist, but there might be an easier way. (It's been awhile since 74 | I've thought about IMAP.) 75 | 76 | (Patches welcome. :-) 77 | 78 | PROBLEMS/COMPLAINTS/SUGGESTIONS 79 | Please use the github issue tracker at: 80 | https://github.com/rtucker/imap2maildir/issues 81 | 82 | MORE INFO 83 | You can download the latest pile of poop at: 84 | https://github.com/rtucker/imap2maildir/ 85 | 86 | -------------------------------------------------------------------------------- /README.changelog: -------------------------------------------------------------------------------- 1 | CHANGES IN 1.10.2 2 | * Adding caching of uids and hashes to cut down on SQL queries. 3 | 4 | CHANGES IN 1.10.1 5 | * A search criteria can be specified, allowing you to download only SEEN 6 | messages, for example (and by default). This may reduce problems with 7 | messages being marked as "read" during downloads. 8 | 9 | CHANGES IN 1.10 10 | * Added support for mbox mailboxes (default is still maildir). You can 11 | either specify "type: mbox" in the config file or use --type=mbox on the 12 | command line. 13 | * There's also another option, --mboxdash, which uses "From -" instead of 14 | "From blah@blah.com" in the From line of mboxes. Useful if your mbox 15 | client freaks out somewhat. 16 | * simpleimap.py now has more parsing guts for parsing IMAP responses, 17 | inspired by http://code.google.com/p/webpymail/. See parseFetch, 18 | parseInternalDate. 19 | * The "turbo" handler now uses lambda instead of that abomination I was 20 | using before. I knew there was an easier way to do that... 21 | * A couple fixes for bugs in Python/imaplib SSL support via SimpleImapSSL. 22 | Had a nice juicy infinite loop thanks to gmail's tendency to drop IMAP 23 | connections after ~10,000 fetches or so. (That was not fun to reproduce) 24 | * Added testsuite.py, which uses unittest to run a couple tests on 25 | simpleimap's parsers. This will be used more in the future. 26 | 27 | CHANGES IN 1.00 28 | The original version is in git as "0.99-beta". 29 | * The IMAP message-handling logic has been abstracted out to simpleimap.py. 30 | This hopes to become its own project to provide an interface to IMAP that 31 | resembles the "mailbox" module. Eventually. Thanks to TJ Fontaine 32 | for the first bits of this! 33 | * The original method of iterating through the messages has been replaced. 34 | The new method appears slower, BUT it is less resource-intensive. 35 | There's plenty of work for optimization on this, I'm sure. 36 | * A new column has been added to the seenmessages table, "uid". This will 37 | store the uid along with the hash and message key, for faster iterating 38 | through established backups. It should "auto-upgrade" itself (I hope). 39 | This allows for the use of TURBO MODE. 40 | * range-start and range-end have gone away, and max-messages now defaults 41 | to 0 (e.g. no limit). I think we're past the point of large mailboxes 42 | causing explosions. 43 | * There is no more high-water-mark logic. Again, this improves stability 44 | for the sake of efficiency. Note that the -m will no longer be quite 45 | as useful (or necessary) as it was before. 46 | * There's a workaround for a SSL bug in some older versions of Python 47 | on Windows. Thanks to Jeff Dean for this, and many other, bug reports!! 48 | 49 | I think this is going to prove a lot more stable, especially for folks who 50 | were running into MemoryError exceptions in imaplib. 51 | 52 | Command line/config file option changes: 53 | Remove --range-start 54 | Remove --range-end 55 | Change default --max-messages to 0 (e.g. infinite) 56 | Replace --force-ssl and --no-ssl with just --ssl 57 | 58 | -------------------------------------------------------------------------------- /example.conf: -------------------------------------------------------------------------------- 1 | # Name this file imap2maildir.conf and life'll be good. 2 | 3 | [imap2maildir] 4 | # Username to use 5 | #username: bobdobbs@gmail.com 6 | 7 | # Password to use (might wanna chmod 600 this file if you set this) 8 | #password: none4u 9 | 10 | # Destination mailbox type 11 | #type: maildir 12 | 13 | # Destination mailbox 14 | #destination: /home/bob/Backups/Gmail 15 | 16 | # Hostname to connect to 17 | #hostname: imap.gmail.com 18 | 19 | # Port to connect to 20 | #port: 993 21 | 22 | # Use SSL? (defaults True) 23 | #ssl: True 24 | 25 | # Remote folder 26 | #remotefolder: [Gmail]/All Mail 27 | 28 | # Debug level (0, 1, or 2) 29 | #debug: 1 30 | 31 | # Maximum number of messages to get in one run (defaults: no limit) 32 | #maxmessages: 1000 33 | 34 | -------------------------------------------------------------------------------- /imap2maildir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Mirrors the contents of an IMAP4 mailbox into a local maildir or mbox. 4 | Intended for keeping a local backup of a remote IMAP4 mailbox to protect 5 | against loss. Very handy for backing up "[Gmail]/All Mail" from your 6 | Gmail account, to snag all your archived mail. Re-running it on a regular 7 | basis will update only the stuff it needs to. 8 | 9 | Once I need to, I'll write a restore script ;-) 10 | 11 | Ryan Tucker 12 | 13 | TODO: 14 | PEP-0008 compliance 15 | - Docstrings 16 | """ 17 | 18 | version = "%prog 1.10.2 20101018" 19 | 20 | try: 21 | from ConfigParser import ConfigParser 22 | except ImportError: 23 | from configparser import ConfigParser 24 | 25 | import email 26 | import getpass 27 | import hashlib 28 | import logging 29 | import mailbox 30 | import optparse 31 | import os 32 | import re 33 | 34 | try: 35 | import rfc822 36 | except ImportError: 37 | import rfc822py3 as rfc822 38 | 39 | import simpleimap 40 | import sqlite3 41 | import sys 42 | import time 43 | 44 | # Handler for logging/debugging/output 45 | log = logging.getLogger(__name__) 46 | console = logging.StreamHandler() 47 | log.addHandler(console) 48 | 49 | # Some reasonable application defaults 50 | defaults = { 51 | 'debug': 1, 52 | 'password': False, 53 | 'hostname': 'imap.gmail.com', 54 | 'ssl': True, 55 | 'port': False, 56 | 'remotefolder': '[Gmail]/All Mail', 57 | 'create': False, 58 | 'maxmessages': 0, 59 | 'configfile': 'imap2maildir.conf', 60 | 'turbo': True, 61 | 'type': 'maildir', 62 | 'mboxdash': False, 63 | 'search': 'SEEN', 64 | } 65 | 66 | class SeenMessagesCache(object): 67 | """ Cache for seen message UIDs and Hashes 68 | """ 69 | def __init__(self): 70 | """ Constructor 71 | """ 72 | 73 | self.uids = None 74 | self.hashes = None 75 | 76 | 77 | class lazyMaildir(mailbox.Maildir): 78 | """ Override the _refresh method, based on patch from 79 | http://bugs.python.org/issue1607951 80 | by A.M. Kuchling, 2009-05-02 81 | """ 82 | 83 | def __init__(self, dirname, factory=rfc822.Message, create=True): 84 | """Initialize a lazy Maildir instance.""" 85 | mailbox.Maildir.__init__(self, dirname, factory, create) 86 | self._last_read = None # Records the last time we read cur/new 87 | 88 | def _refresh(self): 89 | """Update table of contents mapping.""" 90 | new_mtime = os.path.getmtime(os.path.join(self._path, 'new')) 91 | cur_mtime = os.path.getmtime(os.path.join(self._path, 'cur')) 92 | 93 | if (self._last_read is not None and 94 | new_mtime <= self._last_read and cur_mtime <= self._last_read): 95 | return 96 | 97 | self._toc = {} 98 | def update_dir (subdir): 99 | """ update_dir 100 | """ 101 | 102 | path = os.path.join(self._path, subdir) 103 | for entry in os.listdir(path): 104 | p = os.path.join(path, entry) 105 | if os.path.isdir(p): 106 | continue 107 | uniq = entry.split(self.colon)[0] 108 | self._toc[uniq] = os.path.join(subdir, entry) 109 | 110 | update_dir('new') 111 | update_dir('cur') 112 | 113 | # We record the current time - 1sec so that, if _refresh() is called 114 | # again in the same second, we will always re-read the mailbox 115 | # just in case it's been modified. (os.path.mtime() only has 116 | # 1sec resolution.) This results in a few unnecessary re-reads 117 | # when _refresh() is called multiple times in the same second, 118 | # but once the clock ticks over, we will only re-read as needed. 119 | now = int(time.time() - 1) 120 | self._last_read = time.time() - 1 121 | 122 | 123 | def make_hash(size, date, msgid): 124 | """ Returns a hash of a message given the size, date, and msgid thingies. 125 | """ 126 | return hashlib.sha1('%i::%s::%s' % (size, date, msgid)).hexdigest() 127 | 128 | 129 | def open_sql_session(filename): 130 | """ Opens a SQLite database, initializing it if required 131 | """ 132 | 133 | log.debug("Opening sqlite3 database '%s'" % filename) 134 | conn = sqlite3.connect(filename) 135 | c = conn.cursor() 136 | # gather info about the seenmessages table 137 | c.execute('pragma table_info(seenmessages)') 138 | columns = ' '.join(i[1] for i in c.fetchall()).split() 139 | 140 | if columns == []: 141 | # need to create the seenmessages table 142 | c.execute("""create table seenmessages 143 | (hash text not null unique, mailfile text not null, uid integer, folder text)""") 144 | else: 145 | if not 'uid' in columns: 146 | # old db; need to add a column for uid 147 | c.execute("""alter table seenmessages add column uid integer""") 148 | if not 'folder' in columns: 149 | # need to add a column for folder 150 | c.execute("""alter table seenmessages add column folder text""") 151 | 152 | conn.commit() 153 | return conn 154 | 155 | 156 | def check_message(conn, mbox, hash=None, uid=None, seencache=None): 157 | """ Checks to see if a given message exists. 158 | """ 159 | 160 | c = conn.cursor() 161 | if seencache: 162 | if seencache.hashes is None: 163 | # Populate the hash cache 164 | log.debug("Populating hash cache...") 165 | seencache.hashes = {} 166 | c.execute('select hash,folder,mailfile from seenmessages') 167 | for result in c: 168 | seencache.hashes[str(result[0])] = (result[1], result[2]) 169 | log.debug("Hash cache: %i hashes" % len(seencache.hashes)) 170 | if seencache.uids is None: 171 | # Populate the uid cache 172 | log.debug("Populating uid cache...") 173 | seencache.uids = {} 174 | c.execute('select uid,folder,mailfile from seenmessages') 175 | for result in c: 176 | seencache.uids[str(result[0])] = (result[1], result[2]) 177 | log.debug("Uid cache: %i uids" % len(seencache.uids)) 178 | 179 | if hash: 180 | if str(hash) in seencache.hashes: 181 | folder, mailfile = seencache.hashes[hash] 182 | else: 183 | c.execute('select folder,mailfile from seenmessages where hash=?', (hash,)) 184 | row = c.fetchone() 185 | if row: 186 | log.debug("Cache miss on hash %s", hash) 187 | folder, mailfile = row 188 | else: 189 | return False 190 | elif uid: 191 | if str(uid) in seencache.uids: 192 | folder, mailfile = seencache.uids[str(uid)] 193 | else: 194 | c.execute('select folder,mailfile from seenmessages where uid=?', (uid,)) 195 | row = c.fetchone() 196 | if row: 197 | log.debug("Cache miss on uid %s" % uid) 198 | folder, mailfile = row 199 | else: 200 | return False 201 | else: 202 | return False 203 | 204 | if str(mailfile).startswith('POISON-'): 205 | # This is a fake poison filename! Assume truth. 206 | log.warning("Poison filename detected; assuming the message " 207 | "exists and all is well: %s :: %s", 208 | hash or uid, mailfile) 209 | return True 210 | elif isinstance(mbox, mailbox.mbox): 211 | # mailfile will be an int 212 | return int(mailfile) in mbox 213 | elif isinstance(mbox, lazyMaildir): 214 | # mailfile will be a string; use mbox.get because it is faster 215 | if folder: 216 | fmbox = mbox.get_folder(folder) 217 | return fmbox.get(mailfile) 218 | return mbox.get(mailfile) 219 | else: 220 | # uhh let's wing it 221 | return mailfile in mbox 222 | 223 | 224 | def store_hash(conn, hash, mailfile, uid): 225 | """ Given a database connection, hash, mailfile, and uid, 226 | stashes it in the database 227 | """ 228 | 229 | c = conn.cursor() 230 | # nuke it if it's already there. (can happen if disk file goes away) 231 | cur = c.execute('delete from seenmessages where hash = ?', (hash, )) 232 | if cur.rowcount > 0: 233 | log.debug('!!! Nuked duplicate hash %s' % hash) 234 | c.execute('insert into seenmessages values (?,?,?,?)', (hash, mailfile, uid, '')) 235 | conn.commit() 236 | 237 | 238 | def add_uid_to_hash(conn, hash, uid): 239 | """ Adds a uid to a hash that's missing its uid 240 | """ 241 | 242 | c = conn.cursor() 243 | c.execute('update seenmessages set uid = ? where hash = ?', (uid, hash)) 244 | conn.commit() 245 | 246 | 247 | def open_mailbox_maildir(directory, create=False): 248 | """ There is a mailbox here. 249 | """ 250 | 251 | return lazyMaildir(directory, create=create) 252 | 253 | 254 | def open_mailbox_mbox(filename, create=False): 255 | """ Open a mbox file, lock for writing 256 | """ 257 | 258 | mbox = mailbox.mbox(filename, create=create) 259 | mbox.lock() 260 | return mbox 261 | 262 | 263 | def smells_like_maildir(working_dir): 264 | """ Quick check for the cur/tmp/new folders 265 | """ 266 | 267 | return os.path.exists(os.path.join(working_dir, 'cur')) and \ 268 | os.path.exists(os.path.join(working_dir, 'new')) and \ 269 | os.path.exists(os.path.join(working_dir, 'tmp')) 270 | 271 | 272 | def parse_config_file(defaults,configfile='imap2maildir.conf'): 273 | """ Parse config file, if exists. 274 | Returns a tuple with a ConfigParser instance and either True or 275 | False, depending on whether the config was read... 276 | """ 277 | 278 | config = ConfigParser(defaults) 279 | if config.read(configfile): 280 | log.debug('Reading config from ' + configfile) 281 | return (config, True) 282 | else: 283 | log.debug('No config found at ' + configfile) 284 | return (config, False) 285 | 286 | 287 | class FirstOptionParser(optparse.OptionParser): 288 | """ Adjusts parse_args so it won't complain too heavily about 289 | options that don't exist. 290 | Lifted lock, stock, and barrel from /usr/lib/python2.6/optparse.py 291 | """ 292 | 293 | def parse_args(self, args=None, values=None): 294 | """ 295 | parse_args(args : [string] = sys.argv[1:], 296 | values : Values = None) 297 | -> (values : Values, args : [string]) 298 | 299 | Parse the command-line options found in 'args' (default: 300 | sys.argv[1:]). Any errors result in a call to 'error()', which 301 | by default prints the usage message to stderr and calls 302 | sys.exit() with an error message. On success returns a pair 303 | (values, args) where 'values' is an Values instance (with all 304 | your option values) and 'args' is the list of arguments left 305 | over after parsing options. 306 | """ 307 | 308 | rargs = self._get_args(args) 309 | if values is None: 310 | values = self.get_default_values() 311 | self.rargs = rargs 312 | self.largs = largs = [] 313 | self.values = values 314 | 315 | while 1: 316 | try: 317 | stop = self._process_args(largs, rargs, values) 318 | break 319 | except optparse.BadOptionError: 320 | # Just a bad option, let's try this again 321 | pass 322 | except (optparse.OptionValueError) as err: 323 | self.error(str(err)) 324 | 325 | args = largs + rargs 326 | return self.check_values(values, args) 327 | 328 | 329 | def parse_options(defaults): 330 | """ First round of command line parsing: look for a -c option. 331 | """ 332 | 333 | firstparser = FirstOptionParser(add_help_option=False) 334 | firstparser.set_defaults(configfile=defaults['configfile']) 335 | firstparser.add_option("-c", "--config-file", dest="configfile") 336 | (firstoptions, firstargs) = firstparser.parse_args() 337 | 338 | # Parse a config file 339 | (parsedconfig, gotconfig) = parse_config_file( 340 | defaults, configfile=firstoptions.configfile) 341 | 342 | # Parse command line options 343 | usage = "usage: %prog [options]" 344 | description = "A script to copy a remote IMAP folder to a local mail " 345 | description += "storage area. Ideal for incremental backups of mail " 346 | description += "from free webmail providers, or perhaps as an " 347 | description += "alternative to fetchmail. Supports mbox and maildir, " 348 | description += "despite the name. " 349 | description += "See COPYRIGHT for your rights; " 350 | description += "https://github.com/rtucker/imap2maildir/ for info." 351 | if gotconfig: 352 | description = description + '\n\nConfiguration defaults read from \ 353 | file "%s"' % firstoptions.configfile 354 | parser = optparse.OptionParser(usage=usage, version=version, 355 | description=description) 356 | 357 | # Set up some groups 358 | required = optparse.OptionGroup(parser, "Required options") 359 | optional = optparse.OptionGroup(parser, "Optional and debugging options") 360 | 361 | # Set the defaults... 362 | if gotconfig: sectionname = 'imap2maildir' 363 | else: sectionname = 'DEFAULT' 364 | clist = parsedconfig.items(sectionname, raw=True) 365 | for i in clist: 366 | iname = i[0] 367 | if i[1] == 'False': ivalue = False 368 | elif i[1] == 'True': ivalue = True 369 | elif i[0] in ['port', 'debug', 'maxmessages']: ivalue = int(i[1]) 370 | else: ivalue = i[1] 371 | parser.set_default(iname, ivalue) 372 | 373 | # Define the individual options 374 | required.add_option("-u", "--username", dest="username", 375 | help="Username for authentication to IMAP server", metavar="USERNAME") 376 | required.add_option("-d", "--destination", dest="destination", 377 | help="Where to store the mail, e.g. ~/Backups/Gmail", 378 | metavar="PATH") 379 | optional.add_option("-p", "--password", dest="password", 380 | help="Password for IMAP server. Default: prompt user", 381 | metavar="PASSWORD") 382 | optional.add_option("-H", "--hostname", dest="hostname", 383 | help="Hostname of IMAP server, default: %default", metavar="HOSTNAME") 384 | optional.add_option("-P", "--port", dest="port", 385 | help="Port number. Default: 993 (SSL), 143 (clear)", metavar="PORT") 386 | optional.add_option("-v", "--verbose", dest="debug", 387 | help="Turns up the verbosity", action="store_const", const=2) 388 | optional.add_option("-q", "--quiet", dest="debug", 389 | help="Quiets all output (except prompts and errors)", 390 | action="store_const", const=0) 391 | optional.add_option("-r", "--remote-folder", dest="remotefolder", 392 | help="Remote IMAP folder. Default: %default", 393 | metavar="FOLDERNAME") 394 | optional.add_option("-s", "--search", dest="search", 395 | help="IMAP4 search criteria to use. Default: %default", 396 | metavar="CRITERIA") 397 | optional.add_option("--create", dest="create", 398 | help="If --destination doesn't exist, create it", action="store_true") 399 | optional.add_option("--no-turbo", "-T", dest="turbo", 400 | help="Check for message locally before asking IMAP. Default: %default", 401 | action="store_false") 402 | optional.add_option("-m", "--max-messages", dest="maxmessages", 403 | help="How many messages to process in one run (0=infinite). " + 404 | "Default: %default", 405 | metavar="MAX", type="int") 406 | optional.add_option("-c", "--config-file", dest="configfile", 407 | help="Configuration file to use. Default: %default") 408 | optional.add_option("-S", "--ssl", dest="ssl", 409 | help="Use SSL to connect, default: %default", action="store_true") 410 | optional.add_option("-t", "--type", dest="type", action="store", 411 | help="Mailbox type. Choice of: maildir, mbox. Default: %default", 412 | choices=['maildir', 'mbox']) 413 | optional.add_option("--mboxdash", dest="mboxdash", action="store_true", 414 | help="Use - in the mbox From line instead of sender's address. " + 415 | "Default: %default") 416 | 417 | # Parse 418 | parser.add_option_group(required) 419 | parser.add_option_group(optional) 420 | (options, args) = parser.parse_args() 421 | 422 | # Check for required options 423 | if not options.username: 424 | parser.error("Must specify a username (-u/--username).") 425 | if not options.destination: 426 | parser.error("Must specify a destination directory (-d/--destination).") 427 | if not os.path.exists(options.destination): 428 | if options.create: 429 | pass 430 | else: 431 | parser.error("Destination '%s' does not exist. Use --create." 432 | % options.destination) 433 | elif (options.type == 'maildir' 434 | and not smells_like_maildir(options.destination)): 435 | parser.error("Directory '%s' exists, but it isn't a maildir." 436 | % options.destination) 437 | if not options.password: 438 | options.password = getpass.getpass() 439 | 440 | # Set up debugging 441 | if options.debug == 0: 442 | log.setLevel(logging.ERROR) 443 | elif options.debug == 1: 444 | log.setLevel(logging.INFO) 445 | else: 446 | log.setLevel(logging.DEBUG) 447 | 448 | return options 449 | 450 | 451 | def copy_messages_by_folder(folder, db, imap, mbox, limit=0, turbo=False, 452 | mboxdash=False, search=None, seencache=None): 453 | """Copies any messages that haven't yet been seen from imap to mbox. 454 | 455 | copy_messages_by_folder(folder=simpleimap.SimpleImapSSL().Folder(), 456 | db=open_sql_session(), 457 | imap=simpleimap.SimpleImapSSL(), 458 | mbox=open_mailbox_*(), 459 | limit=max number of messages to handle (0 = inf), 460 | turbo=boolean, 461 | mboxdash=use '-' for mbox From line email?, 462 | search=imap criteria (string), 463 | seencache=an object to cache seen messages, 464 | 465 | Returns: {'total': total length of folder, 466 | 'handled': total messages handled, 467 | 'copied': total messages copied, 468 | 'copiedbytes': size of total messages copied, 469 | 'lastuid': last UID seen} 470 | """ 471 | 472 | outdict = {'turbo': 0, 'handled': 0, 'copied': 0, 'copiedbytes': 0, 'lastuid': 0} 473 | outdict['total'] = len(folder) 474 | log.info("Synchronizing %i messages from %s:%s to %s..." % (outdict['total'], folder.host, folder.folder, mbox._path)) 475 | 476 | msgpath = os.path.join(mbox._path, 'new') 477 | 478 | if turbo: 479 | # This will pass the check_message function and some useful cargo 480 | # along to the Summaries() function in the FolderClass. It will 481 | # use this to check the local cache for the message before hitting 482 | # the outside world. (TODO: Make this less suckful.) 483 | log.debug('TURBO MODE ENGAGED!') 484 | folder.__turbo__(lambda uid: check_message(db, mbox, uid=str(uid), seencache=seencache)) 485 | else: 486 | log.debug('Not using turbo mode...') 487 | folder.__turbo__(None) 488 | 489 | # Iterate through the message summary dicts for the folder. 490 | for i in folder.Summaries(search=search): 491 | # i = {'uid': , 'msgid': , 'size': , 'date': } 492 | # Seen it yet? 493 | msghash = make_hash(i['size'], i['date'], i['msgid']) 494 | 495 | if not check_message(db, mbox, hash=msghash, seencache=seencache): 496 | # Hash not found, copy it. 497 | try: 498 | message = imap.get_message_by_uid(i['uid']) 499 | except Exception: 500 | log.exception('ERROR: Could not retrieve message: %s' % repr(i)) 501 | if outdict['handled'] < 1: 502 | log.error("Adding message hash %s to seencache, to avoid " 503 | "future problems...", msghash) 504 | store_hash(db, msghash, 'POISON-%s' % msghash, i['uid']) 505 | add_uid_to_hash(db, msghash, i['uid']) 506 | break 507 | 508 | if mboxdash: 509 | envfrom = '-' 510 | else: 511 | envfrom = i['envfrom'] 512 | message.set_unixfrom("From %s %s" % (envfrom, 513 | time.asctime(imap.parseInternalDate(i['date'])))) 514 | msgfile = mbox.add(message) 515 | store_hash(db, msghash, msgfile, i['uid']) 516 | log.debug(' NEW: ' + repr(i)) 517 | outdict['copied'] += 1 518 | outdict['copiedbytes'] += i['size'] 519 | elif not check_message(db, mbox, uid=str(i['uid']), seencache=seencache): 520 | # UID is missing in the database (old version needs updated) 521 | log.debug('Adding uid %i to msghash %s', i['uid'], msghash) 522 | add_uid_to_hash(db, msghash, i['uid']) 523 | else: 524 | log.debug('Unexpected turbo mode on uid %i', i['uid']) 525 | 526 | # Update our counters. 527 | outdict['handled'] += 1 528 | outdict['turbo'] = folder.turbocounter() 529 | 530 | if outdict['handled'] % 100 == 0: 531 | percentage = ((outdict['handled'] + outdict['turbo'])/ float(outdict['total'])) * 100 532 | log.info('Copied: %i, Turbo: %i, Seen: %i (%i%%, latest UID %i, date %s)' % 533 | (outdict['copied'], outdict['turbo'], outdict['handled'], 534 | percentage, i['uid'], i['date'])) 535 | outdict['lastuid'] = i['uid'] 536 | if (outdict['handled'] >= limit) and (limit > 0): 537 | log.info('Limit of %i messages reached' % limit) 538 | break 539 | 540 | # Make sure this gets updated... 541 | outdict['turbo'] = folder.turbocounter() 542 | return outdict 543 | 544 | 545 | def main(): 546 | """ main loop 547 | """ 548 | 549 | log.debug('Hello. Version %s' % version) 550 | # Parse the command line and config file 551 | options = parse_options(defaults) 552 | 553 | # Check to make sure the mailbox type is valid (probably redundant) 554 | if options.type not in ['maildir', 'mbox']: 555 | raise ValueError("No valid mailbox type specified") 556 | 557 | # Open mailbox and database, and copy messages 558 | try: 559 | if options.type == 'maildir': 560 | mbox = open_mailbox_maildir(options.destination, options.create) 561 | db = open_sql_session(os.path.join(options.destination, '.imap2maildir.sqlite')) 562 | elif options.type == 'mbox': 563 | mbox = open_mailbox_mbox(options.destination, options.create) 564 | db = open_sql_session(options.destination + '.sqlite') 565 | 566 | seencache = SeenMessagesCache() 567 | 568 | # Connect to IMAP server 569 | imapserver = simpleimap.Server(hostname=options.hostname, 570 | username=options.username, password=options.password, 571 | port=options.port, ssl=options.ssl) 572 | imap = imapserver.Get() 573 | 574 | # Instantiate a folder 575 | folder = imap.Folder(folder=options.remotefolder) 576 | folder.__keepaliver__(imapserver.Keepalive) 577 | 578 | result = copy_messages_by_folder(folder=folder, 579 | db=db, 580 | imap=imap, 581 | mbox=mbox, 582 | limit=options.maxmessages, 583 | turbo=options.turbo, 584 | mboxdash=options.mboxdash, 585 | search=options.search, 586 | seencache=seencache) 587 | except (KeyboardInterrupt, SystemExit): 588 | log.warning('Caught interrupt; clearing locks and safing database.') 589 | mbox.unlock() 590 | db.rollback() 591 | raise 592 | except: 593 | log.exception('Exception! Clearing locks and safing database.') 594 | mbox.unlock() 595 | db.rollback() 596 | raise 597 | 598 | # Unlock the mailbox if locked. 599 | mbox.unlock() 600 | 601 | # Print results. 602 | log.info('FINISHED: Turboed %(turbo)i, handled %(handled)i, copied %(copied)i (%(copiedbytes)i bytes), last UID was %(lastuid)i' % result) 603 | 604 | if __name__ == "__main__": 605 | main() 606 | 607 | -------------------------------------------------------------------------------- /pydoc/imap2maildir.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module imap2maildir 4 | 5 | 6 | 7 | 8 |
 
9 |  
imap2maildir
index
f:\development\portablegit\imap2maildir\imap2maildir.py
12 |

Mirrors the contents of an IMAP4 mailbox into a local maildir or mbox.
13 | Intended for keeping a local backup of a remote IMAP4 mailbox to protect
14 | against loss.  Very handy for backing up "[Gmail]/All Mail" from your
15 | Gmail account, to snag all your archived mail.  Re-running it on a regular
16 | basis will update only the stuff it needs to.
17 |  
18 | Once I need to, I'll write a restore script ;-)
19 |  
20 | Ryan Tucker <rtucker@gmail.com>
21 |  
22 | TODO:
23 | PEP-0008 compliance
24 |   - Docstrings

25 |

26 | 27 | 28 | 30 | 31 | 32 |
 
29 | Modules
       
ConfigParser
33 | email
34 | getpass
35 | hashlib
36 |
logging
37 | mailbox
38 | optparse
39 | os
40 |
re
41 | rfc822
42 | simpleimap
43 | sqlite3
44 |
sys
45 | time
46 |

47 | 48 | 49 | 51 | 52 | 53 |
 
50 | Classes
       
54 |
__builtin__.object 55 |
56 |
57 |
SeenMessagesCache 58 |
59 |
60 |
mailbox.Maildir(mailbox.Mailbox) 61 |
62 |
63 |
lazyMaildir 64 |
65 |
66 |
optparse.OptionParser(optparse.OptionContainer) 67 |
68 |
69 |
FirstOptionParser 70 |
71 |
72 |
73 |

74 | 75 | 76 | 78 | 79 | 80 | 83 | 84 |
 
77 | class FirstOptionParser(optparse.OptionParser)
   Adjusts parse_args so it won't complain too heavily about
81 | options that don't exist.
82 | Lifted lock, stock, and barrel from /usr/lib/python2.6/optparse.py
 
 
Method resolution order:
85 |
FirstOptionParser
86 |
optparse.OptionParser
87 |
optparse.OptionContainer
88 |
89 |
90 | Methods defined here:
91 |
parse_args(self, args=None, values=None)
parse_args(args : [string] = sys.argv[1:],
92 |            values : Values = None)
93 | -> (values : Values, args : [string])
94 |  
95 | Parse the command-line options found in 'args' (default:
96 | sys.argv[1:]).  Any errors result in a call to 'error()', which
97 | by default prints the usage message to stderr and calls
98 | sys.exit() with an error message.  On success returns a pair
99 | (values, args) where 'values' is an Values instance (with all
100 | your option values) and 'args' is the list of arguments left
101 | over after parsing options.
102 | 103 |
104 | Methods inherited from optparse.OptionParser:
105 |
__init__(self, usage=None, option_list=None, option_class=<class optparse.Option>, version=None, conflict_handler='error', description=None, formatter=None, add_help_option=True, prog=None, epilog=None)
106 | 107 |
add_option_group(self, *args, **kwargs)
108 | 109 |
check_values(self, values, args)
check_values(values : Values, args : [string])
110 | -> (values : Values, args : [string])
111 |  
112 | Check that the supplied option values and leftover arguments are
113 | valid.  Returns the option values and leftover arguments
114 | (possibly adjusted, possibly completely new -- whatever you
115 | like).  Default implementation just returns the passed-in
116 | values; subclasses may override as desired.
117 | 118 |
destroy(self)
Declare that you are done with this OptionParser.  This cleans up
119 | reference cycles so the OptionParser (and all objects referenced by
120 | it) can be garbage-collected promptly.  After calling destroy(), the
121 | OptionParser is unusable.
122 | 123 |
disable_interspersed_args(self)
Set parsing to stop on the first non-option. Use this if
124 | you have a command processor which runs another command that
125 | has options of its own and you want to make sure these options
126 | don't get confused.
127 | 128 |
enable_interspersed_args(self)
Set parsing to not stop on the first non-option, allowing
129 | interspersing switches with command arguments. This is the
130 | default behavior. See also disable_interspersed_args() and the
131 | class documentation description of the attribute
132 | allow_interspersed_args.
133 | 134 |
error(self, msg)
error(msg : string)
135 |  
136 | Print a usage message incorporating 'msg' to stderr and exit.
137 | If you override this in a subclass, it should not return -- it
138 | should either exit or raise an exception.
139 | 140 |
exit(self, status=0, msg=None)
141 | 142 |
expand_prog_name(self, s)
143 | 144 |
format_epilog(self, formatter)
145 | 146 |
format_help(self, formatter=None)
147 | 148 |
format_option_help(self, formatter=None)
149 | 150 |
get_default_values(self)
151 | 152 |
get_description(self)
153 | 154 |
get_option_group(self, opt_str)
155 | 156 |
get_prog_name(self)
157 | 158 |
get_usage(self)
159 | 160 |
get_version(self)
161 | 162 |
print_help(self, file=None)
print_help(file : file = stdout)
163 |  
164 | Print an extended help message, listing all options and any
165 | help text provided with them, to 'file' (default stdout).
166 | 167 |
print_usage(self, file=None)
print_usage(file : file = stdout)
168 |  
169 | Print the usage message for the current program (self.usage) to
170 | 'file' (default stdout).  Any occurrence of the string "%prog" in
171 | self.usage is replaced with the name of the current program
172 | (basename of sys.argv[0]).  Does nothing if self.usage is empty
173 | or not defined.
174 | 175 |
print_version(self, file=None)
print_version(file : file = stdout)
176 |  
177 | Print the version message for this program (self.version) to
178 | 'file' (default stdout).  As with print_usage(), any occurrence
179 | of "%prog" in self.version is replaced by the current program's
180 | name.  Does nothing if self.version is empty or undefined.
181 | 182 |
set_default(self, dest, value)
183 | 184 |
set_defaults(self, **kwargs)
185 | 186 |
set_process_default_values(self, process)
187 | 188 |
set_usage(self, usage)
189 | 190 |
191 | Data and other attributes inherited from optparse.OptionParser:
192 |
standard_option_list = []
193 | 194 |
195 | Methods inherited from optparse.OptionContainer:
196 |
add_option(self, *args, **kwargs)
add_option(Option)
197 | add_option(opt_str, ..., kwarg=val, ...)
198 | 199 |
add_options(self, option_list)
200 | 201 |
format_description(self, formatter)
202 | 203 |
get_option(self, opt_str)
204 | 205 |
has_option(self, opt_str)
206 | 207 |
remove_option(self, opt_str)
208 | 209 |
set_conflict_handler(self, handler)
210 | 211 |
set_description(self, description)
212 | 213 |

214 | 215 | 216 | 218 | 219 | 220 | 221 | 222 |
 
217 | class SeenMessagesCache(__builtin__.object)
   Cache for seen message UIDs and Hashes
 
 Methods defined here:
223 |
__init__(self)
Constructor
224 | 225 |
226 | Data descriptors defined here:
227 |
__dict__
228 |
dictionary for instance variables (if defined)
229 |
230 |
__weakref__
231 |
list of weak references to the object (if defined)
232 |
233 |

234 | 235 | 236 | 238 | 239 | 240 | 243 | 244 |
 
237 | class lazyMaildir(mailbox.Maildir)
   Override the _refresh method, based on patch from
241 | http://bugs.python.org/issue1607951
242 | by A.M. Kuchling, 2009-05-02
 
 
Method resolution order:
245 |
lazyMaildir
246 |
mailbox.Maildir
247 |
mailbox.Mailbox
248 |
249 |
250 | Methods defined here:
251 |
__init__(self, dirname, factory=<class rfc822.Message>, create=True)
Initialize a lazy Maildir instance.
252 | 253 |
254 | Methods inherited from mailbox.Maildir:
255 |
__len__(self)
Return a count of messages in the mailbox.
256 | 257 |
__setitem__(self, key, message)
Replace the keyed message; raise KeyError if it doesn't exist.
258 | 259 |
add(self, message)
Add message and return assigned key.
260 | 261 |
add_folder(self, folder)
Create a folder and return a Maildir instance representing it.
262 | 263 |
clean(self)
Delete old files in "tmp".
264 | 265 |
close(self)
Flush and close the mailbox.
266 | 267 |
discard(self, key)
If the keyed message exists, remove it.
268 | 269 |
flush(self)
Write any pending changes to disk.
270 | 271 |
get_file(self, key)
Return a file-like representation or raise a KeyError.
272 | 273 |
get_folder(self, folder)
Return a Maildir instance for the named folder.
274 | 275 |
get_message(self, key)
Return a Message representation or raise a KeyError.
276 | 277 |
get_string(self, key)
Return a string representation or raise a KeyError.
278 | 279 |
has_key(self, key)
Return True if the keyed message exists, False otherwise.
280 | 281 |
iterkeys(self)
Return an iterator over keys.
282 | 283 |
list_folders(self)
Return a list of folder names.
284 | 285 |
lock(self)
Lock the mailbox.
286 | 287 |
next(self)
Return the next message in a one-time iteration.
288 | 289 |
remove(self, key)
Remove the keyed message; raise KeyError if it doesn't exist.
290 | 291 |
remove_folder(self, folder)
Delete the named folder, which must be empty.
292 | 293 |
unlock(self)
Unlock the mailbox if it is locked.
294 | 295 |
296 | Data and other attributes inherited from mailbox.Maildir:
297 |
colon = ':'
298 | 299 |
300 | Methods inherited from mailbox.Mailbox:
301 |
__contains__(self, key)
302 | 303 |
__delitem__(self, key)
304 | 305 |
__getitem__(self, key)
Return the keyed message; raise KeyError if it doesn't exist.
306 | 307 |
__iter__(self)
308 | 309 |
clear(self)
Delete all messages.
310 | 311 |
get(self, key, default=None)
Return the keyed message, or default if it doesn't exist.
312 | 313 |
items(self)
Return a list of (key, message) tuples. Memory intensive.
314 | 315 |
iteritems(self)
Return an iterator over (key, message) tuples.
316 | 317 |
itervalues(self)
Return an iterator over all messages.
318 | 319 |
keys(self)
Return a list of keys.
320 | 321 |
pop(self, key, default=None)
Delete the keyed message and return it, or default.
322 | 323 |
popitem(self)
Delete an arbitrary (key, message) pair and return it.
324 | 325 |
update(self, arg=None)
Change the messages that correspond to certain keys.
326 | 327 |
values(self)
Return a list of messages. Memory intensive.
328 | 329 |

330 | 331 | 332 | 334 | 335 | 336 |
 
333 | Functions
       
add_uid_to_hash(conn, hash, uid)
Adds a uid to a hash that's missing its uid
337 |
check_message(conn, mbox, hash=None, uid=None, seencache=None)
Checks to see if a given message exists.
338 |
copy_messages_by_folder(folder, db, imap, mbox, limit=0, turbo=False, mboxdash=False, search=None, seencache=None)
Copies any messages that haven't yet been seen from imap to mbox.
339 |  
340 | copy_messages_by_folder(folder=simpleimap.SimpleImapSSL().Folder(),
341 |                         db=open_sql_session(),
342 |                         imap=simpleimap.SimpleImapSSL(),
343 |                         mbox=open_mailbox_*(),
344 |                         limit=max number of messages to handle (0 = inf),
345 |                         turbo=boolean,
346 |                         mboxdash=use '-' for mbox From line email?,
347 |                         search=imap criteria (string),
348 |                         seencache=an object to cache seen messages,
349 |  
350 | Returns: {'total': total length of folder,
351 |           'handled': total messages handled,
352 |           'copied': total messages copied,
353 |           'copiedbytes': size of total messages copied,
354 |           'lastuid': last UID seen}
355 |
main()
main loop
356 |
make_hash(size, date, msgid)
Returns a hash of a message given the size, date, and msgid thingies.
357 |
open_mailbox_maildir(directory, create=False)
There is a mailbox here.
358 |
open_mailbox_mbox(filename, create=False)
Open a mbox file, lock for writing
359 |
open_sql_session(filename)
Opens a SQLite database, initializing it if required
360 |
parse_config_file(defaults, configfile='imap2maildir.conf')
Parse config file, if exists.
361 | Returns a tuple with a ConfigParser instance and either True or
362 | False, depending on whether the config was read...
363 |
parse_options(defaults)
First round of command line parsing: look for a -c option.
364 |
smells_like_maildir(working_dir)
Quick check for the cur/tmp/new folders
365 |
store_hash(conn, hash, mailfile, uid)
Given a database connection, hash, mailfile, and uid,
366 | stashes it in the database
367 |

368 | 369 | 370 | 372 | 373 | 374 |
 
371 | Data
       console = <logging.StreamHandler object>
375 | defaults = {'configfile': 'imap2maildir.conf', 'create': False, 'debug': 1, 'hostname': 'imap.gmail.com', 'maxmessages': 0, 'mboxdash': False, 'password': False, 'port': False, 'remotefolder': '[Gmail]/All Mail', 'search': 'SEEN', ...}
376 | log = <logging.Logger object>
377 | version = '%prog 1.10.2 20101018'
378 | -------------------------------------------------------------------------------- /pydoc/simpleimap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module simpleimap 4 | 5 | 6 | 7 | 8 |
 
9 |  
simpleimap
index
f:\development\portablegit\imap2maildir\simpleimap.py
12 |

simpleimap.py, originally from http://p.linode.com/2693 on 2009/07/22
13 | Copyright (c) 2009 Timothy J Fontaine <tjfontaine@gmail.com>
14 | Copyright (c) 2009 Ryan S. Tucker <rtucker@gmail.com>

15 |

16 | 17 | 18 | 20 | 21 | 22 |
 
19 | Modules
       
email
23 | imaplib
24 |
logging
25 | platform
26 |
re
27 | time
28 |

29 | 30 | 31 | 33 | 34 | 35 |
 
32 | Classes
       
36 |
imaplib.IMAP4 37 |
38 |
39 |
SimpleImap(imaplib.IMAP4, __simplebase) 40 |
41 |
42 |
imaplib.IMAP4_SSL(imaplib.IMAP4) 43 |
44 |
45 |
SimpleImapSSL(imaplib.IMAP4_SSL, __simplebase) 46 |
47 |
48 |
FolderClass 49 |
Server 50 |
__simplebase 51 |
52 |
53 |
SimpleImap(imaplib.IMAP4, __simplebase) 54 |
SimpleImapSSL(imaplib.IMAP4_SSL, __simplebase) 55 |
56 |
57 |
58 |

59 | 60 | 61 | 63 | 64 | 65 | 71 | 72 |
 
62 | class FolderClass
   Class for instantiating a folder instance.
66 |  
67 | TODO: Trap exceptions like:
68 | ssl.SSLError: [Errno 8] _ssl.c:1325: EOF occurred in violation of protocol
69 | by trying to reconnect to the server.
70 | (Raised up via get_summary_by_uid in Summaries when IMAP server boogers.)
 
 Methods defined here:
73 |
Ids(self, search='ALL')
Ids
74 | 75 |
Messages(self, search='ALL')
Messsages
76 | 77 |
Summaries(self, search='ALL')
Summaries
78 | 79 |
Uids(self, search='ALL')
Uids
80 | 81 |
__init__(self, parent, folder='INBOX', charset=None)
82 | 83 |
__keepaliver__(self, keepaliver)
__keep aliver
84 | 85 |
__keepaliver_none__(self)
__keepaliver_none__
86 | 87 |
__len__(self)
__len__
88 | 89 |
__turbo__(self, turbofunction)
Calls turbofunction(uid) for every uid, only yielding those
90 | where turbofunction returns False.  Set to None to disable.
91 | 92 |
turbocounter(self, reset=False)
turbocounter
93 | 94 |

95 | 96 | 97 | 99 | 100 | 101 | 102 | 103 |
 
98 | class Server
   Class for instantiating a server instance
 
 Methods defined here:
104 |
Connect(self)
Connect
105 | 106 |
Get(self)
Get
107 | 108 |
Keepalive(self)
Call me occasionally just to make sure everything's OK...
109 | 110 |
__init__(self, hostname=None, username=None, password=None, port=None, ssl=True)
Constructor
111 | 112 |

113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 |
 
116 | class SimpleImap(imaplib.IMAP4, __simplebase)
   Simple Imap
 
 
Method resolution order:
122 |
SimpleImap
123 |
imaplib.IMAP4
124 |
__simplebase
125 |
126 |
127 | Methods inherited from imaplib.IMAP4:
128 |
__getattr__(self, attr)
129 | 130 |
__init__(self, host='', port=143)
131 | 132 |
append(self, mailbox, flags, date_time, message)
Append message to named mailbox.
133 |  
134 | (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
135 |  
136 |         All args except `message' can be None.
137 | 138 |
authenticate(self, mechanism, authobject)
Authenticate command - requires response processing.
139 |  
140 | 'mechanism' specifies which authentication mechanism is to
141 | be used - it must appear in <instance>.capabilities in the
142 | form AUTH=<mechanism>.
143 |  
144 | 'authobject' must be a callable object:
145 |  
146 |         data = authobject(response)
147 |  
148 | It will be called to process server continuation responses.
149 | It should return data that will be encoded and sent to server.
150 | It should return None if the client abort response '*' should
151 | be sent instead.
152 | 153 |
capability(self)
(typ, [data]) = <instance>.capability()
154 | Fetch capabilities list from server.
155 | 156 |
check(self)
Checkpoint mailbox on server.
157 |  
158 | (typ, [data]) = <instance>.check()
159 | 160 |
close(self)
Close currently selected mailbox.
161 |  
162 | Deleted messages are removed from writable mailbox.
163 | This is the recommended command before 'LOGOUT'.
164 |  
165 | (typ, [data]) = <instance>.close()
166 | 167 |
copy(self, message_set, new_mailbox)
Copy 'message_set' messages onto end of 'new_mailbox'.
168 |  
169 | (typ, [data]) = <instance>.copy(message_set, new_mailbox)
170 | 171 |
create(self, mailbox)
Create new mailbox.
172 |  
173 | (typ, [data]) = <instance>.create(mailbox)
174 | 175 |
delete(self, mailbox)
Delete old mailbox.
176 |  
177 | (typ, [data]) = <instance>.delete(mailbox)
178 | 179 |
deleteacl(self, mailbox, who)
Delete the ACLs (remove any rights) set for who on mailbox.
180 |  
181 | (typ, [data]) = <instance>.deleteacl(mailbox, who)
182 | 183 |
expunge(self)
Permanently remove deleted items from selected mailbox.
184 |  
185 | Generates 'EXPUNGE' response for each deleted message.
186 |  
187 | (typ, [data]) = <instance>.expunge()
188 |  
189 | 'data' is list of 'EXPUNGE'd message numbers in order received.
190 | 191 |
fetch(self, message_set, message_parts)
Fetch (parts of) messages.
192 |  
193 | (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
194 |  
195 | 'message_parts' should be a string of selected parts
196 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
197 |  
198 | 'data' are tuples of message part envelope and data.
199 | 200 |
getacl(self, mailbox)
Get the ACLs for a mailbox.
201 |  
202 | (typ, [data]) = <instance>.getacl(mailbox)
203 | 204 |
getannotation(self, mailbox, entry, attribute)
(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
205 | Retrieve ANNOTATIONs.
206 | 207 |
getquota(self, root)
Get the quota root's resource usage and limits.
208 |  
209 | Part of the IMAP4 QUOTA extension defined in rfc2087.
210 |  
211 | (typ, [data]) = <instance>.getquota(root)
212 | 213 |
getquotaroot(self, mailbox)
Get the list of quota roots for the named mailbox.
214 |  
215 | (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
216 | 217 |
list(self, directory='""', pattern='*')
List mailbox names in directory matching pattern.
218 |  
219 | (typ, [data]) = <instance>.list(directory='""', pattern='*')
220 |  
221 | 'data' is list of LIST responses.
222 | 223 |
login(self, user, password)
Identify client using plaintext password.
224 |  
225 | (typ, [data]) = <instance>.login(user, password)
226 |  
227 | NB: 'password' will be quoted.
228 | 229 |
login_cram_md5(self, user, password)
Force use of CRAM-MD5 authentication.
230 |  
231 | (typ, [data]) = <instance>.login_cram_md5(user, password)
232 | 233 |
logout(self)
Shutdown connection to server.
234 |  
235 | (typ, [data]) = <instance>.logout()
236 |  
237 | Returns server 'BYE' response.
238 | 239 |
lsub(self, directory='""', pattern='*')
List 'subscribed' mailbox names in directory matching pattern.
240 |  
241 | (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
242 |  
243 | 'data' are tuples of message part envelope and data.
244 | 245 |
myrights(self, mailbox)
Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
246 |  
247 | (typ, [data]) = <instance>.myrights(mailbox)
248 | 249 |
namespace(self)
Returns IMAP namespaces ala rfc2342
250 |  
251 | (typ, [data, ...]) = <instance>.namespace()
252 | 253 |
noop(self)
Send NOOP command.
254 |  
255 | (typ, [data]) = <instance>.noop()
256 | 257 |
open(self, host='', port=143)
Setup connection to remote server on "host:port"
258 |     (default: localhost:standard IMAP4 port).
259 | This connection will be used by the routines:
260 |     read, readline, send, shutdown.
261 | 262 |
partial(self, message_num, message_part, start, length)
Fetch truncated part of a message.
263 |  
264 | (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
265 |  
266 | 'data' is tuple of message part envelope and data.
267 | 268 |
print_log(self)
269 | 270 |
proxyauth(self, user)
Assume authentication as "user".
271 |  
272 | Allows an authorised administrator to proxy into any user's
273 | mailbox.
274 |  
275 | (typ, [data]) = <instance>.proxyauth(user)
276 | 277 |
read(self, size)
Read 'size' bytes from remote.
278 | 279 |
readline(self)
Read line from remote.
280 | 281 |
recent(self)
Return most recent 'RECENT' responses if any exist,
282 | else prompt server for an update using the 'NOOP' command.
283 |  
284 | (typ, [data]) = <instance>.recent()
285 |  
286 | 'data' is None if no new messages,
287 | else list of RECENT responses, most recent last.
288 | 289 |
rename(self, oldmailbox, newmailbox)
Rename old mailbox name to new.
290 |  
291 | (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
292 | 293 |
response(self, code)
Return data for response 'code' if received, or None.
294 |  
295 | Old value for response 'code' is cleared.
296 |  
297 | (code, [data]) = <instance>.response(code)
298 | 299 |
search(self, charset, *criteria)
Search mailbox for matching messages.
300 |  
301 | (typ, [data]) = <instance>.search(charset, criterion, ...)
302 |  
303 | 'data' is space separated list of matching message numbers.
304 | 305 |
select(self, mailbox='INBOX', readonly=False)
Select a mailbox.
306 |  
307 | Flush all untagged responses.
308 |  
309 | (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
310 |  
311 | 'data' is count of messages in mailbox ('EXISTS' response).
312 |  
313 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
314 | other responses should be obtained via <instance>.response('FLAGS') etc.
315 | 316 |
send(self, data)
Send data to remote.
317 | 318 |
setacl(self, mailbox, who, what)
Set a mailbox acl.
319 |  
320 | (typ, [data]) = <instance>.setacl(mailbox, who, what)
321 | 322 |
setannotation(self, *args)
(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
323 | Set ANNOTATIONs.
324 | 325 |
setquota(self, root, limits)
Set the quota root's resource limits.
326 |  
327 | (typ, [data]) = <instance>.setquota(root, limits)
328 | 329 |
shutdown(self)
Close I/O established in "open".
330 | 331 |
socket(self)
Return socket instance used to connect to IMAP4 server.
332 |  
333 | socket = <instance>.socket()
334 | 335 |
sort(self, sort_criteria, charset, *search_criteria)
IMAP4rev1 extension SORT command.
336 |  
337 | (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
338 | 339 |
status(self, mailbox, names)
Request named status conditions for mailbox.
340 |  
341 | (typ, [data]) = <instance>.status(mailbox, names)
342 | 343 |
store(self, message_set, command, flags)
Alters flag dispositions for messages in mailbox.
344 |  
345 | (typ, [data]) = <instance>.store(message_set, command, flags)
346 | 347 |
subscribe(self, mailbox)
Subscribe to new mailbox.
348 |  
349 | (typ, [data]) = <instance>.subscribe(mailbox)
350 | 351 |
thread(self, threading_algorithm, charset, *search_criteria)
IMAPrev1 extension THREAD command.
352 |  
353 | (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
354 | 355 |
uid(self, command, *args)
Execute "command arg ..." with messages identified by UID,
356 |         rather than message number.
357 |  
358 | (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
359 |  
360 | Returns response appropriate to 'command'.
361 | 362 |
unsubscribe(self, mailbox)
Unsubscribe from old mailbox.
363 |  
364 | (typ, [data]) = <instance>.unsubscribe(mailbox)
365 | 366 |
xatom(self, name, *args)
Allow simple extension commands
367 |         notified by server in CAPABILITY response.
368 |  
369 | Assumes command is legal in current state.
370 |  
371 | (typ, [data]) = <instance>.xatom(name, arg, ...)
372 |  
373 | Returns response appropriate to extension command `name'.
374 | 375 |
376 | Data and other attributes inherited from imaplib.IMAP4:
377 |
abort = <class 'imaplib.abort'>
378 | 379 |
error = <class 'imaplib.error'>
380 | 381 |
mustquote = <_sre.SRE_Pattern object>
382 | 383 |
readonly = <class 'imaplib.readonly'>
384 | 385 |
386 | Methods inherited from __simplebase:
387 |
Folder(self, folder, charset=None)
Returns an instance of FolderClass.
388 | 389 |
get_ids_by_folder(self, folder, charset=None, search='ALL')
get ids by folder
390 | 391 |
get_message_by_id(self, id)
get_message_by_id
392 | 393 |
get_message_by_uid(self, uid)
get_message_by_uid
394 | 395 |
get_messages_by_folder(self, folder, charset=None, search='ALL')
get messages by folder
396 | 397 |
get_messages_by_ids(self, ids)
get messages by ids
398 | 399 |
get_messages_by_uids(self, uids)
get messages by uids
400 | 401 |
get_summaries_by_folder(self, folder, charset=None, search='ALL')
get summaries by folder
402 | 403 |
get_summaries_by_ids(self, ids)
get summaries by ids
404 | 405 |
get_summaries_by_uids(self, uids)
get summaries by uids
406 | 407 |
get_summary_by_id(self, id)
Retrieve a dictionary of simple header information for a given id.
408 |  
409 | Requires: id (Sequence number of message)
410 | Returns: {'uid': UID you requested,
411 |           'msgid': RFC822 Message ID,
412 |           'size': Size of message in bytes,
413 |           'date': IMAP's Internaldate for the message,
414 |           'envelope': Envelope data}
415 | 416 |
get_summary_by_uid(self, uid)
Retrieve a dictionary of simple header information for a given uid.
417 |  
418 | Requires: uid (unique numeric ID of message)
419 | Returns: {'uid': UID you requested,
420 |           'msgid': RFC822 Message ID,
421 |           'size': Size of message in bytes,
422 |           'date': IMAP's Internaldate for the message,
423 |           'envelope': Envelope data}
424 | 425 |
get_uid_by_id(self, id)
Given a message number (id), returns the UID if it exists.
426 | 427 |
get_uids_by_folder(self, folder, charset=None, search='ALL')
get_uids by folders
428 | 429 |
get_uids_by_ids(self, ids)
get uids by ids
430 | 431 |
parseFetch(self, text)
Given a string (e.g. '1 (ENVELOPE...'), breaks it down into
432 | a useful format.
433 |  
434 | Based on Helder Guerreiro <helder@paxjulia.com>'s
435 | imaplib2 sexp.py: http://code.google.com/p/webpymail/
436 | 437 |
parseInternalDate(self, resp)
Takes IMAP INTERNALDATE and turns it into a Python time
438 | tuple referenced to GMT.
439 |  
440 | Based from: http://code.google.com/p/webpymail/
441 | 442 |
parse_summary_data(self, data)
Takes the data result (second parameter) of a self.uid or
443 | self.fetch for (UID ENVELOPE RFC822.SIZE INTERNALDATE) and returns
444 | a dict of simple header information.
445 |  
446 | Requires: self.uid[1] or self.fetch[1]
447 | Returns: {'uid': UID you requested,
448 |           'msgid': RFC822 Message ID,
449 |           'size': Size of message in bytes,
450 |           'date': IMAP's Internaldate for the message,
451 |           'envelope': Envelope data}
452 | 453 |

454 | 455 | 456 | 458 | 459 | 460 | 461 | 462 |
 
457 | class SimpleImapSSL(imaplib.IMAP4_SSL, __simplebase)
   Simple Imap SSL
 
 
Method resolution order:
463 |
SimpleImapSSL
464 |
imaplib.IMAP4_SSL
465 |
imaplib.IMAP4
466 |
__simplebase
467 |
468 |
469 | Methods defined here:
470 |
read(self, n)
Read 'size' bytes from remote.  (Contains workaround)
471 | 472 |
473 | Methods inherited from imaplib.IMAP4_SSL:
474 |
__init__(self, host='', port=993, keyfile=None, certfile=None)
475 | 476 |
open(self, host='', port=993)
Setup connection to remote server on "host:port".
477 |     (default: localhost:standard IMAP4 SSL port).
478 | This connection will be used by the routines:
479 |     read, readline, send, shutdown.
480 | 481 |
readline(self)
Read line from remote.
482 | 483 |
send(self, data)
Send data to remote.
484 | 485 |
shutdown(self)
Close I/O established in "open".
486 | 487 |
socket(self)
Return socket instance used to connect to IMAP4 server.
488 |  
489 | socket = <instance>.socket()
490 | 491 |
ssl(self)
Return SSLObject instance used to communicate with the IMAP4 server.
492 |  
493 | ssl = ssl.wrap_socket(<instance>.socket)
494 | 495 |
496 | Methods inherited from imaplib.IMAP4:
497 |
__getattr__(self, attr)
498 | 499 |
append(self, mailbox, flags, date_time, message)
Append message to named mailbox.
500 |  
501 | (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
502 |  
503 |         All args except `message' can be None.
504 | 505 |
authenticate(self, mechanism, authobject)
Authenticate command - requires response processing.
506 |  
507 | 'mechanism' specifies which authentication mechanism is to
508 | be used - it must appear in <instance>.capabilities in the
509 | 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.
516 | It should return data that will be encoded and sent to server.
517 | It should return None if the client abort response '*' should
518 | be sent instead.
519 | 520 |
capability(self)
(typ, [data]) = <instance>.capability()
521 | Fetch capabilities list from server.
522 | 523 |
check(self)
Checkpoint mailbox on server.
524 |  
525 | (typ, [data]) = <instance>.check()
526 | 527 |
close(self)
Close currently selected mailbox.
528 |  
529 | Deleted messages are removed from writable mailbox.
530 | This is the recommended command before 'LOGOUT'.
531 |  
532 | (typ, [data]) = <instance>.close()
533 | 534 |
copy(self, message_set, new_mailbox)
Copy 'message_set' messages onto end of 'new_mailbox'.
535 |  
536 | (typ, [data]) = <instance>.copy(message_set, new_mailbox)
537 | 538 |
create(self, mailbox)
Create new mailbox.
539 |  
540 | (typ, [data]) = <instance>.create(mailbox)
541 | 542 |
delete(self, mailbox)
Delete old mailbox.
543 |  
544 | (typ, [data]) = <instance>.delete(mailbox)
545 | 546 |
deleteacl(self, mailbox, who)
Delete the ACLs (remove any rights) set for who on mailbox.
547 |  
548 | (typ, [data]) = <instance>.deleteacl(mailbox, who)
549 | 550 |
expunge(self)
Permanently remove deleted items from selected mailbox.
551 |  
552 | Generates 'EXPUNGE' response for each deleted message.
553 |  
554 | (typ, [data]) = <instance>.expunge()
555 |  
556 | 'data' is list of 'EXPUNGE'd message numbers in order received.
557 | 558 |
fetch(self, message_set, message_parts)
Fetch (parts of) messages.
559 |  
560 | (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
561 |  
562 | 'message_parts' should be a string of selected parts
563 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
564 |  
565 | 'data' are tuples of message part envelope and data.
566 | 567 |
getacl(self, mailbox)
Get the ACLs for a mailbox.
568 |  
569 | (typ, [data]) = <instance>.getacl(mailbox)
570 | 571 |
getannotation(self, mailbox, entry, attribute)
(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
572 | Retrieve ANNOTATIONs.
573 | 574 |
getquota(self, root)
Get the quota root's resource usage and limits.
575 |  
576 | Part of the IMAP4 QUOTA extension defined in rfc2087.
577 |  
578 | (typ, [data]) = <instance>.getquota(root)
579 | 580 |
getquotaroot(self, mailbox)
Get the list of quota roots for the named mailbox.
581 |  
582 | (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
583 | 584 |
list(self, directory='""', pattern='*')
List mailbox names in directory matching pattern.
585 |  
586 | (typ, [data]) = <instance>.list(directory='""', pattern='*')
587 |  
588 | 'data' is list of LIST responses.
589 | 590 |
login(self, user, password)
Identify client using plaintext password.
591 |  
592 | (typ, [data]) = <instance>.login(user, password)
593 |  
594 | NB: 'password' will be quoted.
595 | 596 |
login_cram_md5(self, user, password)
Force use of CRAM-MD5 authentication.
597 |  
598 | (typ, [data]) = <instance>.login_cram_md5(user, password)
599 | 600 |
logout(self)
Shutdown connection to server.
601 |  
602 | (typ, [data]) = <instance>.logout()
603 |  
604 | Returns server 'BYE' response.
605 | 606 |
lsub(self, directory='""', pattern='*')
List 'subscribed' mailbox names in directory matching pattern.
607 |  
608 | (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
609 |  
610 | 'data' are tuples of message part envelope and data.
611 | 612 |
myrights(self, mailbox)
Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
613 |  
614 | (typ, [data]) = <instance>.myrights(mailbox)
615 | 616 |
namespace(self)
Returns IMAP namespaces ala rfc2342
617 |  
618 | (typ, [data, ...]) = <instance>.namespace()
619 | 620 |
noop(self)
Send NOOP command.
621 |  
622 | (typ, [data]) = <instance>.noop()
623 | 624 |
partial(self, message_num, message_part, start, length)
Fetch truncated part of a message.
625 |  
626 | (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
627 |  
628 | 'data' is tuple of message part envelope and data.
629 | 630 |
print_log(self)
631 | 632 |
proxyauth(self, user)
Assume authentication as "user".
633 |  
634 | Allows an authorised administrator to proxy into any user's
635 | mailbox.
636 |  
637 | (typ, [data]) = <instance>.proxyauth(user)
638 | 639 |
recent(self)
Return most recent 'RECENT' responses if any exist,
640 | else prompt server for an update using the 'NOOP' command.
641 |  
642 | (typ, [data]) = <instance>.recent()
643 |  
644 | 'data' is None if no new messages,
645 | else list of RECENT responses, most recent last.
646 | 647 |
rename(self, oldmailbox, newmailbox)
Rename old mailbox name to new.
648 |  
649 | (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
650 | 651 |
response(self, code)
Return data for response 'code' if received, or None.
652 |  
653 | Old value for response 'code' is cleared.
654 |  
655 | (code, [data]) = <instance>.response(code)
656 | 657 |
search(self, charset, *criteria)
Search mailbox for matching messages.
658 |  
659 | (typ, [data]) = <instance>.search(charset, criterion, ...)
660 |  
661 | 'data' is space separated list of matching message numbers.
662 | 663 |
select(self, mailbox='INBOX', readonly=False)
Select a mailbox.
664 |  
665 | Flush all untagged responses.
666 |  
667 | (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
668 |  
669 | 'data' is count of messages in mailbox ('EXISTS' response).
670 |  
671 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
672 | other responses should be obtained via <instance>.response('FLAGS') etc.
673 | 674 |
setacl(self, mailbox, who, what)
Set a mailbox acl.
675 |  
676 | (typ, [data]) = <instance>.setacl(mailbox, who, what)
677 | 678 |
setannotation(self, *args)
(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
679 | Set ANNOTATIONs.
680 | 681 |
setquota(self, root, limits)
Set the quota root's resource limits.
682 |  
683 | (typ, [data]) = <instance>.setquota(root, limits)
684 | 685 |
sort(self, sort_criteria, charset, *search_criteria)
IMAP4rev1 extension SORT command.
686 |  
687 | (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
688 | 689 |
status(self, mailbox, names)
Request named status conditions for mailbox.
690 |  
691 | (typ, [data]) = <instance>.status(mailbox, names)
692 | 693 |
store(self, message_set, command, flags)
Alters flag dispositions for messages in mailbox.
694 |  
695 | (typ, [data]) = <instance>.store(message_set, command, flags)
696 | 697 |
subscribe(self, mailbox)
Subscribe to new mailbox.
698 |  
699 | (typ, [data]) = <instance>.subscribe(mailbox)
700 | 701 |
thread(self, threading_algorithm, charset, *search_criteria)
IMAPrev1 extension THREAD command.
702 |  
703 | (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
704 | 705 |
uid(self, command, *args)
Execute "command arg ..." with messages identified by UID,
706 |         rather than message number.
707 |  
708 | (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
709 |  
710 | Returns response appropriate to 'command'.
711 | 712 |
unsubscribe(self, mailbox)
Unsubscribe from old mailbox.
713 |  
714 | (typ, [data]) = <instance>.unsubscribe(mailbox)
715 | 716 |
xatom(self, name, *args)
Allow simple extension commands
717 |         notified by server in CAPABILITY response.
718 |  
719 | Assumes command is legal in current state.
720 |  
721 | (typ, [data]) = <instance>.xatom(name, arg, ...)
722 |  
723 | Returns response appropriate to extension command `name'.
724 | 725 |
726 | Data and other attributes inherited from imaplib.IMAP4:
727 |
abort = <class 'imaplib.abort'>
728 | 729 |
error = <class 'imaplib.error'>
730 | 731 |
mustquote = <_sre.SRE_Pattern object>
732 | 733 |
readonly = <class 'imaplib.readonly'>
734 | 735 |
736 | Methods inherited from __simplebase:
737 |
Folder(self, folder, charset=None)
Returns an instance of FolderClass.
738 | 739 |
get_ids_by_folder(self, folder, charset=None, search='ALL')
get ids by folder
740 | 741 |
get_message_by_id(self, id)
get_message_by_id
742 | 743 |
get_message_by_uid(self, uid)
get_message_by_uid
744 | 745 |
get_messages_by_folder(self, folder, charset=None, search='ALL')
get messages by folder
746 | 747 |
get_messages_by_ids(self, ids)
get messages by ids
748 | 749 |
get_messages_by_uids(self, uids)
get messages by uids
750 | 751 |
get_summaries_by_folder(self, folder, charset=None, search='ALL')
get summaries by folder
752 | 753 |
get_summaries_by_ids(self, ids)
get summaries by ids
754 | 755 |
get_summaries_by_uids(self, uids)
get summaries by uids
756 | 757 |
get_summary_by_id(self, id)
Retrieve a dictionary of simple header information for a given id.
758 |  
759 | Requires: id (Sequence number of message)
760 | Returns: {'uid': UID you requested,
761 |           'msgid': RFC822 Message ID,
762 |           'size': Size of message in bytes,
763 |           'date': IMAP's Internaldate for the message,
764 |           'envelope': Envelope data}
765 | 766 |
get_summary_by_uid(self, uid)
Retrieve a dictionary of simple header information for a given uid.
767 |  
768 | Requires: uid (unique numeric ID of message)
769 | Returns: {'uid': UID you requested,
770 |           'msgid': RFC822 Message ID,
771 |           'size': Size of message in bytes,
772 |           'date': IMAP's Internaldate for the message,
773 |           'envelope': Envelope data}
774 | 775 |
get_uid_by_id(self, id)
Given a message number (id), returns the UID if it exists.
776 | 777 |
get_uids_by_folder(self, folder, charset=None, search='ALL')
get_uids by folders
778 | 779 |
get_uids_by_ids(self, ids)
get uids by ids
780 | 781 |
parseFetch(self, text)
Given a string (e.g. '1 (ENVELOPE...'), breaks it down into
782 | a useful format.
783 |  
784 | Based on Helder Guerreiro <helder@paxjulia.com>'s
785 | imaplib2 sexp.py: http://code.google.com/p/webpymail/
786 | 787 |
parseInternalDate(self, resp)
Takes IMAP INTERNALDATE and turns it into a Python time
788 | tuple referenced to GMT.
789 |  
790 | Based from: http://code.google.com/p/webpymail/
791 | 792 |
parse_summary_data(self, data)
Takes the data result (second parameter) of a self.uid or
793 | self.fetch for (UID ENVELOPE RFC822.SIZE INTERNALDATE) and returns
794 | a dict of simple header information.
795 |  
796 | Requires: self.uid[1] or self.fetch[1]
797 | Returns: {'uid': UID you requested,
798 |           'msgid': RFC822 Message ID,
799 |           'size': Size of message in bytes,
800 |           'date': IMAP's Internaldate for the message,
801 |           'envelope': Envelope data}
802 | 803 |

804 | -------------------------------------------------------------------------------- /pydoc/testsuite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module testsuite 4 | 5 | 6 | 7 | 8 |
 
9 |  
testsuite
index
f:\development\portablegit\imap2maildir\testsuite.py
12 |

Runs various tests on stuff.

13 |

14 | 15 | 16 | 18 | 19 | 20 |
 
17 | Modules
       
simpleimap
21 |
unittest
22 |

23 | 24 | 25 | 27 | 28 | 29 |
 
26 | Classes
       
30 |
unittest.case.TestCase(__builtin__.object) 31 |
32 |
33 |
TestParseSummaryData 34 |
35 |
36 |
37 |

38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 |
 
41 | class TestParseSummaryData(unittest.case.TestCase)
   Test Parse Summary Data
 
 
Method resolution order:
47 |
TestParseSummaryData
48 |
unittest.case.TestCase
49 |
__builtin__.object
50 |
51 |
52 | Methods defined here:
53 |
setUp(self)
create an instance
54 | 55 |
testEmbeddedSubjectFiveBackslashes(self)
Tests a message with five (!) backslashes in the Subject.
56 | >>> imap.uid('FETCH', 57455, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)')
57 | 58 |
testEmbeddedSubjectQuotedDoubleBackslash(self)
Tests a message with embedded double backslash inside quotes, in the Subject.
59 |  
60 | >>> imap.uid('FETCH', 120818, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)')
61 |  
62 | From https://github.com/rtucker/imap2maildir/issues#issue/10
63 |     "blablabla\"
64 | 65 |
testEmbeddedSubjectQuotes(self)
Tests a message with embedded double quotes in the Subject.
66 |  
67 | >>> imap.uid('FETCH', 57454, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)')
68 | 69 |
testInReplyTo(self)
Test a message with an in-reply-to, for the correct message ID.
70 | >>> imap.uid('FETCH', 17264, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)')
71 | 72 |
73 | Methods inherited from unittest.case.TestCase:
74 |
__call__(self, *args, **kwds)
75 | 76 |
__eq__(self, other)
77 | 78 |
__hash__(self)
79 | 80 |
__init__(self, methodName='runTest')
Create an instance of the class that will use the named test
81 | method when executed. Raises a ValueError if the instance does
82 | not have a method with the specified name.
83 | 84 |
__ne__(self, other)
85 | 86 |
__repr__(self)
87 | 88 |
__str__(self)
89 | 90 |
addCleanup(self, function, *args, **kwargs)
Add a function, with arguments, to be called when the test is
91 | completed. Functions added are called on a LIFO basis and are
92 | called after tearDown on test failure or success.
93 |  
94 | Cleanup items are called even if setUp fails (unlike tearDown).
95 | 96 |
addTypeEqualityFunc(self, typeobj, function)
Add a type specific assertEqual style function to compare a type.
97 |  
98 | This method is for use by TestCase subclasses that need to register
99 | their own type equality functions to provide nicer error messages.
100 |  
101 | Args:
102 |     typeobj: The data type to call this function on when both values
103 |             are of the same type in assertEqual().
104 |     function: The callable taking two arguments and an optional
105 |             msg= argument that raises self.failureException with a
106 |             useful error message when the two arguments are not equal.
107 | 108 |
assertAlmostEqual(self, first, second, places=None, msg=None, delta=None)
Fail if the two objects are unequal as determined by their
109 | difference rounded to the given number of decimal places
110 | (default 7) and comparing to zero, or by comparing that the
111 | between the two objects is more than the given delta.
112 |  
113 | Note that decimal places (from zero) are usually not the same
114 | as significant digits (measured from the most signficant digit).
115 |  
116 | If the two objects compare equal then they will automatically
117 | compare almost equal.
118 | 119 |
assertAlmostEquals = assertAlmostEqual(self, first, second, places=None, msg=None, delta=None)
Fail if the two objects are unequal as determined by their
120 | difference rounded to the given number of decimal places
121 | (default 7) and comparing to zero, or by comparing that the
122 | between the two objects is more than the given delta.
123 |  
124 | Note that decimal places (from zero) are usually not the same
125 | as significant digits (measured from the most signficant digit).
126 |  
127 | If the two objects compare equal then they will automatically
128 | compare almost equal.
129 | 130 |
assertDictContainsSubset(self, expected, actual, msg=None)
Checks whether actual is a superset of expected.
131 | 132 |
assertDictEqual(self, d1, d2, msg=None)
133 | 134 |
assertEqual(self, first, second, msg=None)
Fail if the two objects are unequal as determined by the '=='
135 | operator.
136 | 137 |
assertEquals = assertEqual(self, first, second, msg=None)
Fail if the two objects are unequal as determined by the '=='
138 | operator.
139 | 140 |
assertFalse(self, expr, msg=None)
Fail the test if the expression is true.
141 | 142 |
assertGreater(self, a, b, msg=None)
Just like assertTrue(a > b), but with a nicer default message.
143 | 144 |
assertGreaterEqual(self, a, b, msg=None)
Just like assertTrue(a >= b), but with a nicer default message.
145 | 146 |
assertIn(self, member, container, msg=None)
Just like assertTrue(a in b), but with a nicer default message.
147 | 148 |
assertIs(self, expr1, expr2, msg=None)
Just like assertTrue(a is b), but with a nicer default message.
149 | 150 |
assertIsInstance(self, obj, cls, msg=None)
Same as assertTrue(isinstance(obj, cls)), with a nicer
151 | default message.
152 | 153 |
assertIsNone(self, obj, msg=None)
Same as assertTrue(obj is None), with a nicer default message.
154 | 155 |
assertIsNot(self, expr1, expr2, msg=None)
Just like assertTrue(a is not b), but with a nicer default message.
156 | 157 |
assertIsNotNone(self, obj, msg=None)
Included for symmetry with assertIsNone.
158 | 159 |
assertItemsEqual(self, expected_seq, actual_seq, msg=None)
An unordered sequence / set specific comparison. It asserts that
160 | expected_seq and actual_seq contain the same elements. It is
161 | the equivalent of::
162 |  
163 |     assertEqual(sorted(expected_seq), sorted(actual_seq))
164 |  
165 | Raises with an error message listing which elements of expected_seq
166 | are missing from actual_seq and vice versa if any.
167 |  
168 | Asserts that each element has the same count in both sequences.
169 | Example:
170 |     - [0, 1, 1] and [1, 0, 1] compare equal.
171 |     - [0, 0, 1] and [0, 1] compare unequal.
172 | 173 |
assertLess(self, a, b, msg=None)
Just like assertTrue(a < b), but with a nicer default message.
174 | 175 |
assertLessEqual(self, a, b, msg=None)
Just like assertTrue(a <= b), but with a nicer default message.
176 | 177 |
assertListEqual(self, list1, list2, msg=None)
A list-specific equality assertion.
178 |  
179 | Args:
180 |     list1: The first list to compare.
181 |     list2: The second list to compare.
182 |     msg: Optional message to use on failure instead of a list of
183 |             differences.
184 | 185 |
assertMultiLineEqual(self, first, second, msg=None)
Assert that two multi-line strings are equal.
186 | 187 |
assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None)
Fail if the two objects are equal as determined by their
188 | difference rounded to the given number of decimal places
189 | (default 7) and comparing to zero, or by comparing that the
190 | between the two objects is less than the given delta.
191 |  
192 | Note that decimal places (from zero) are usually not the same
193 | as significant digits (measured from the most signficant digit).
194 |  
195 | Objects that are equal automatically fail.
196 | 197 |
assertNotAlmostEquals = assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None)
Fail if the two objects are equal as determined by their
198 | difference rounded to the given number of decimal places
199 | (default 7) and comparing to zero, or by comparing that the
200 | between the two objects is less than the given delta.
201 |  
202 | Note that decimal places (from zero) are usually not the same
203 | as significant digits (measured from the most signficant digit).
204 |  
205 | Objects that are equal automatically fail.
206 | 207 |
assertNotEqual(self, first, second, msg=None)
Fail if the two objects are equal as determined by the '=='
208 | operator.
209 | 210 |
assertNotEquals = assertNotEqual(self, first, second, msg=None)
Fail if the two objects are equal as determined by the '=='
211 | operator.
212 | 213 |
assertNotIn(self, member, container, msg=None)
Just like assertTrue(a not in b), but with a nicer default message.
214 | 215 |
assertNotIsInstance(self, obj, cls, msg=None)
Included for symmetry with assertIsInstance.
216 | 217 |
assertNotRegexpMatches(self, text, unexpected_regexp, msg=None)
Fail the test if the text matches the regular expression.
218 | 219 |
assertRaises(self, excClass, callableObj=None, *args, **kwargs)
Fail unless an exception of class excClass is thrown
220 | by callableObj when invoked with arguments args and keyword
221 | arguments kwargs. If a different type of exception is
222 | thrown, it will not be caught, and the test case will be
223 | deemed to have suffered an error, exactly as for an
224 | unexpected exception.
225 |  
226 | If called with callableObj omitted or None, will return a
227 | context object used like this::
228 |  
229 |      with assertRaises(SomeException):
230 |          do_something()
231 |  
232 | The context manager keeps a reference to the exception as
233 | the 'exception' attribute. This allows you to inspect the
234 | exception after the assertion::
235 |  
236 |     with assertRaises(SomeException) as cm:
237 |         do_something()
238 |     the_exception = cm.exception
239 |     assertEqual(the_exception.error_code, 3)
240 | 241 |
assertRaisesRegexp(self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs)
Asserts that the message in a raised exception matches a regexp.
242 |  
243 | Args:
244 |     expected_exception: Exception class expected to be raised.
245 |     expected_regexp: Regexp (re pattern object or string) expected
246 |             to be found in error message.
247 |     callable_obj: Function to be called.
248 |     args: Extra args.
249 |     kwargs: Extra kwargs.
250 | 251 |
assertRegexpMatches(self, text, expected_regexp, msg=None)
Fail the test unless the text matches the regular expression.
252 | 253 |
assertSequenceEqual(self, seq1, seq2, msg=None, seq_type=None)
An equality assertion for ordered sequences (like lists and tuples).
254 |  
255 | For the purposes of this function, a valid ordered sequence type is one
256 | which can be indexed, has a length, and has an equality operator.
257 |  
258 | Args:
259 |     seq1: The first sequence to compare.
260 |     seq2: The second sequence to compare.
261 |     seq_type: The expected datatype of the sequences, or None if no
262 |             datatype should be enforced.
263 |     msg: Optional message to use on failure instead of a list of
264 |             differences.
265 | 266 |
assertSetEqual(self, set1, set2, msg=None)
A set-specific equality assertion.
267 |  
268 | Args:
269 |     set1: The first set to compare.
270 |     set2: The second set to compare.
271 |     msg: Optional message to use on failure instead of a list of
272 |             differences.
273 |  
274 | assertSetEqual uses ducktyping to support different types of sets, and
275 | is optimized for sets specifically (parameters must support a
276 | difference method).
277 | 278 |
assertTrue(self, expr, msg=None)
Fail the test unless the expression is true.
279 | 280 |
assertTupleEqual(self, tuple1, tuple2, msg=None)
A tuple-specific equality assertion.
281 |  
282 | Args:
283 |     tuple1: The first tuple to compare.
284 |     tuple2: The second tuple to compare.
285 |     msg: Optional message to use on failure instead of a list of
286 |             differences.
287 | 288 |
assert_ = assertTrue(self, expr, msg=None)
Fail the test unless the expression is true.
289 | 290 |
countTestCases(self)
291 | 292 |
debug(self)
Run the test without collecting errors in a TestResult
293 | 294 |
defaultTestResult(self)
295 | 296 |
doCleanups(self)
Execute all cleanup functions. Normally called for you after
297 | tearDown.
298 | 299 |
fail(self, msg=None)
Fail immediately, with the given message.
300 | 301 |
failIf = deprecated_func(*args, **kwargs)
302 | 303 |
failIfAlmostEqual = deprecated_func(*args, **kwargs)
304 | 305 |
failIfEqual = deprecated_func(*args, **kwargs)
306 | 307 |
failUnless = deprecated_func(*args, **kwargs)
308 | 309 |
failUnlessAlmostEqual = deprecated_func(*args, **kwargs)
310 | 311 |
failUnlessEqual = deprecated_func(*args, **kwargs)
312 | 313 |
failUnlessRaises = deprecated_func(*args, **kwargs)
314 | 315 |
id(self)
316 | 317 |
run(self, result=None)
318 | 319 |
shortDescription(self)
Returns a one-line description of the test, or None if no
320 | description has been provided.
321 |  
322 | The default implementation of this method returns the first line of
323 | the specified test method's docstring.
324 | 325 |
skipTest(self, reason)
Skip this test.
326 | 327 |
tearDown(self)
Hook method for deconstructing the test fixture after testing it.
328 | 329 |
330 | Class methods inherited from unittest.case.TestCase:
331 |
setUpClass(cls) from __builtin__.type
Hook method for setting up class fixture before running tests in the class.
332 | 333 |
tearDownClass(cls) from __builtin__.type
Hook method for deconstructing the class fixture after running all tests in the class.
334 | 335 |
336 | Data descriptors inherited from unittest.case.TestCase:
337 |
__dict__
338 |
dictionary for instance variables (if defined)
339 |
340 |
__weakref__
341 |
list of weak references to the object (if defined)
342 |
343 |
344 | Data and other attributes inherited from unittest.case.TestCase:
345 |
failureException = <type 'exceptions.AssertionError'>
Assertion failed.
346 | 347 |
longMessage = False
348 | 349 |
maxDiff = 640
350 | 351 |

352 | -------------------------------------------------------------------------------- /rfc822py3.py: -------------------------------------------------------------------------------- 1 | """RFC 2822 message manipulation (compatible with Python 3.x). 2 | 3 | Note: This is only a very rough sketch of a full RFC-822 parser; in particular 4 | the tokenizing of addresses does not adhere to all the quoting rules. 5 | 6 | Note: RFC 2822 is a long awaited update to RFC 822. This module should 7 | conform to RFC 2822, and is thus mis-named (it's not worth renaming it). Some 8 | effort at RFC 2822 updates have been made, but a thorough audit has not been 9 | performed. Consider any RFC 2822 non-conformance to be a bug. 10 | 11 | RFC 2822: http://www.faqs.org/rfcs/rfc2822.html 12 | RFC 822 : http://www.faqs.org/rfcs/rfc822.html (obsolete) 13 | 14 | Directions for use: 15 | 16 | To create a Message object: first open a file, e.g.: 17 | 18 | fp = open(file, 'r') 19 | 20 | You can use any other legal way of getting an open file object, e.g. use 21 | sys.stdin or call os.popen(). Then pass the open file object to the Message() 22 | constructor: 23 | 24 | m = Message(fp) 25 | 26 | This class can work with any input object that supports a readline method. If 27 | the input object has seek and tell capability, the rewindbody method will 28 | work; also illegal lines will be pushed back onto the input stream. If the 29 | input object lacks seek but has an `unread' method that can push back a line 30 | of input, Message will use that to push back illegal lines. Thus this class 31 | can be used to parse messages coming from a buffered stream. 32 | 33 | The optional `seekable' argument is provided as a workaround for certain stdio 34 | libraries in which tell() discards buffered data before discovering that the 35 | lseek() system call doesn't work. For maximum portability, you should set the 36 | seekable argument to zero to prevent that initial \code{tell} when passing in 37 | an unseekable object such as a a file object created from a socket object. If 38 | it is 1 on entry -- which it is by default -- the tell() method of the open 39 | file object is called once; if this raises an exception, seekable is reset to 40 | 0. For other nonzero values of seekable, this test is not made. 41 | 42 | To get the text of a particular header there are several methods: 43 | 44 | str = m.getheader(name) 45 | str = m.getrawheader(name) 46 | 47 | where name is the name of the header, e.g. 'Subject'. The difference is that 48 | getheader() strips the leading and trailing whitespace, while getrawheader() 49 | doesn't. Both functions retain embedded whitespace (including newlines) 50 | exactly as they are specified in the header, and leave the case of the text 51 | unchanged. 52 | 53 | For addresses and address lists there are functions 54 | 55 | realname, mailaddress = m.getaddr(name) 56 | list = m.getaddrlist(name) 57 | 58 | where the latter returns a list of (realname, mailaddr) tuples. 59 | 60 | There is also a method 61 | 62 | time = m.getdate(name) 63 | 64 | which parses a Date-like field and returns a time-compatible tuple, 65 | i.e. a tuple such as returned by time.localtime() or accepted by 66 | time.mktime(). 67 | 68 | See the class definition for lower level access methods. 69 | 70 | There are also some utility functions here. 71 | 72 | - Cleanup and extensions by Eric S. Raymond 73 | - Ported to Python 3 by Mark J. Nenadov (Apr 16, 2011) 74 | 75 | """ 76 | 77 | import time 78 | 79 | __all__ = ["Message","AddressList","parsedate","parsedate_tz","mktime_tz"] 80 | 81 | _blanklines = ('\r\n', '\n') # Optimization for islast() 82 | 83 | 84 | class Message: 85 | """Represents a single RFC 2822-compliant message.""" 86 | 87 | def __init__(self, fp, seekable = 1): 88 | """Initialize the class instance and read the headers.""" 89 | if seekable == 1: 90 | # Exercise tell() to make sure it works 91 | # (and then assume seek() works, too) 92 | try: 93 | fp.tell() 94 | except (AttributeError, IOError): 95 | seekable = 0 96 | self.fp = fp 97 | self.seekable = seekable 98 | self.startofheaders = None 99 | self.startofbody = None 100 | # 101 | if self.seekable: 102 | try: 103 | self.startofheaders = self.fp.tell() 104 | except IOError: 105 | self.seekable = 0 106 | # 107 | self.readheaders() 108 | # 109 | if self.seekable: 110 | try: 111 | self.startofbody = self.fp.tell() 112 | except IOError: 113 | self.seekable = 0 114 | 115 | def rewindbody(self): 116 | """Rewind the file to the start of the body (if seekable).""" 117 | if not self.seekable: 118 | raise IOError("unseekable file") 119 | self.fp.seek(self.startofbody) 120 | 121 | def readheaders(self): 122 | """Read header lines. 123 | 124 | Read header lines up to the entirely blank line that terminates them. 125 | The (normally blank) line that ends the headers is skipped, but not 126 | included in the returned list. If a non-header line ends the headers, 127 | (which is an error), an attempt is made to backspace over it; it is 128 | never included in the returned list. 129 | 130 | The variable self.status is set to the empty string if all went well, 131 | otherwise it is an error message. The variable self.headers is a 132 | completely uninterpreted list of lines contained in the header (so 133 | printing them will reproduce the header exactly as it appears in the 134 | file). 135 | """ 136 | self.dict = {} 137 | self.unixfrom = '' 138 | self.headers = lst = [] 139 | self.status = '' 140 | headerseen = "" 141 | firstline = 1 142 | startofline = unread = tell = None 143 | if hasattr(self.fp, 'unread'): 144 | unread = self.fp.unread 145 | elif self.seekable: 146 | tell = self.fp.tell 147 | while 1: 148 | if tell: 149 | try: 150 | startofline = tell() 151 | except IOError: 152 | startofline = tell = None 153 | self.seekable = 0 154 | line = self.fp.readline() 155 | if not line: 156 | self.status = 'EOF in headers' 157 | break 158 | # Skip unix From name time lines 159 | if firstline and line.startswith('From '): 160 | self.unixfrom = self.unixfrom + line 161 | continue 162 | firstline = 0 163 | if headerseen and line[0] in ' \t': 164 | # It's a continuation line. 165 | lst.append(line) 166 | x = (self.dict[headerseen] + "\n " + line.strip()) 167 | self.dict[headerseen] = x.strip() 168 | continue 169 | elif self.iscomment(line): 170 | # It's a comment. Ignore it. 171 | continue 172 | elif self.islast(line): 173 | # Note! No pushback here! The delimiter line gets eaten. 174 | break 175 | headerseen = self.isheader(line) 176 | if headerseen: 177 | # It's a legal header line, save it. 178 | lst.append(line) 179 | self.dict[headerseen] = line[len(headerseen)+1:].strip() 180 | continue 181 | else: 182 | # It's not a header line; throw it back and stop here. 183 | if not self.dict: 184 | self.status = 'No headers' 185 | else: 186 | self.status = 'Non-header line where header expected' 187 | # Try to undo the read. 188 | if unread: 189 | unread(line) 190 | elif tell: 191 | self.fp.seek(startofline) 192 | else: 193 | self.status = self.status + '; bad seek' 194 | break 195 | 196 | def isheader(self, line): 197 | """Determine whether a given line is a legal header. 198 | 199 | This method should return the header name, suitably canonicalized. 200 | You may override this method in order to use Message parsing on tagged 201 | data in RFC 2822-like formats with special header formats. 202 | """ 203 | i = line.find(':') 204 | if i > 0: 205 | return line[:i].lower() 206 | return None 207 | 208 | def islast(self, line): 209 | """Determine whether a line is a legal end of RFC 2822 headers. 210 | 211 | You may override this method if your application wants to bend the 212 | rules, e.g. to strip trailing whitespace, or to recognize MH template 213 | separators ('--------'). For convenience (e.g. for code reading from 214 | sockets) a line consisting of \r\n also matches. 215 | """ 216 | return line in _blanklines 217 | 218 | def iscomment(self, line): 219 | """Determine whether a line should be skipped entirely. 220 | 221 | You may override this method in order to use Message parsing on tagged 222 | data in RFC 2822-like formats that support embedded comments or 223 | free-text data. 224 | """ 225 | return False 226 | 227 | def getallmatchingheaders(self, name): 228 | """Find all header lines matching a given header name. 229 | 230 | Look through the list of headers and find all lines matching a given 231 | header name (and their continuation lines). A list of the lines is 232 | returned, without interpretation. If the header does not occur, an 233 | empty list is returned. If the header occurs multiple times, all 234 | occurrences are returned. Case is not important in the header name. 235 | """ 236 | name = name.lower() + ':' 237 | n = len(name) 238 | lst = [] 239 | hit = 0 240 | for line in self.headers: 241 | if line[:n].lower() == name: 242 | hit = 1 243 | elif not line[:1].isspace(): 244 | hit = 0 245 | if hit: 246 | lst.append(line) 247 | return lst 248 | 249 | def getfirstmatchingheader(self, name): 250 | """Get the first header line matching name. 251 | 252 | This is similar to getallmatchingheaders, but it returns only the 253 | first matching header (and its continuation lines). 254 | """ 255 | name = name.lower() + ':' 256 | n = len(name) 257 | lst = [] 258 | hit = 0 259 | for line in self.headers: 260 | if hit: 261 | if not line[:1].isspace(): 262 | break 263 | elif line[:n].lower() == name: 264 | hit = 1 265 | if hit: 266 | lst.append(line) 267 | return lst 268 | 269 | def getrawheader(self, name): 270 | """A higher-level interface to getfirstmatchingheader(). 271 | 272 | Return a string containing the literal text of the header but with the 273 | keyword stripped. All leading, trailing and embedded whitespace is 274 | kept in the string, however. Return None if the header does not 275 | occur. 276 | """ 277 | 278 | lst = self.getfirstmatchingheader(name) 279 | if not lst: 280 | return None 281 | lst[0] = lst[0][len(name) + 1:] 282 | return ''.join(lst) 283 | 284 | def getheader(self, name, default=None): 285 | """Get the header value for a name. 286 | 287 | This is the normal interface: it returns a stripped version of the 288 | header value for a given header name, or None if it doesn't exist. 289 | This uses the dictionary version which finds the *last* such header. 290 | """ 291 | return self.dict.get(name.lower(), default) 292 | get = getheader 293 | 294 | def getheaders(self, name): 295 | """Get all values for a header. 296 | 297 | This returns a list of values for headers given more than once; each 298 | value in the result list is stripped in the same way as the result of 299 | getheader(). If the header is not given, return an empty list. 300 | """ 301 | result = [] 302 | current = '' 303 | have_header = 0 304 | for s in self.getallmatchingheaders(name): 305 | if s[0].isspace(): 306 | if current: 307 | current = "%s\n %s" % (current, s.strip()) 308 | else: 309 | current = s.strip() 310 | else: 311 | if have_header: 312 | result.append(current) 313 | current = s[s.find(":") + 1:].strip() 314 | have_header = 1 315 | if have_header: 316 | result.append(current) 317 | return result 318 | 319 | def getaddr(self, name): 320 | """Get a single address from a header, as a tuple. 321 | 322 | An example return value: 323 | ('Guido van Rossum', 'guido@cwi.nl') 324 | """ 325 | # New, by Ben Escoto 326 | alist = self.getaddrlist(name) 327 | if alist: 328 | return alist[0] 329 | else: 330 | return (None, None) 331 | 332 | def getaddrlist(self, name): 333 | """Get a list of addresses from a header. 334 | 335 | Retrieves a list of addresses from a header, where each address is a 336 | tuple as returned by getaddr(). Scans all named headers, so it works 337 | properly with multiple To: or Cc: headers for example. 338 | """ 339 | raw = [] 340 | for h in self.getallmatchingheaders(name): 341 | if h[0] in ' \t': 342 | raw.append(h) 343 | else: 344 | if raw: 345 | raw.append(', ') 346 | i = h.find(':') 347 | if i > 0: 348 | addr = h[i+1:] 349 | raw.append(addr) 350 | alladdrs = ''.join(raw) 351 | a = AddressList(alladdrs) 352 | return a.addresslist 353 | 354 | def getdate(self, name): 355 | """Retrieve a date field from a header. 356 | 357 | Retrieves a date field from the named header, returning a tuple 358 | compatible with time.mktime(). 359 | """ 360 | try: 361 | data = self[name] 362 | except KeyError: 363 | return None 364 | return parsedate(data) 365 | 366 | def getdate_tz(self, name): 367 | """Retrieve a date field from a header as a 10-tuple. 368 | 369 | The first 9 elements make up a tuple compatible with time.mktime(), 370 | and the 10th is the offset of the poster's time zone from GMT/UTC. 371 | """ 372 | try: 373 | data = self[name] 374 | except KeyError: 375 | return None 376 | return parsedate_tz(data) 377 | 378 | 379 | # Access as a dictionary (only finds *last* header of each type): 380 | 381 | def __len__(self): 382 | """Get the number of headers in a message.""" 383 | return len(self.dict) 384 | 385 | def __getitem__(self, name): 386 | """Get a specific header, as from a dictionary.""" 387 | return self.dict[name.lower()] 388 | 389 | def __setitem__(self, name, value): 390 | """Set the value of a header. 391 | 392 | Note: This is not a perfect inversion of __getitem__, because any 393 | changed headers get stuck at the end of the raw-headers list rather 394 | than where the altered header was. 395 | """ 396 | del self[name] # Won't fail if it doesn't exist 397 | self.dict[name.lower()] = value 398 | text = name + ": " + value 399 | for line in text.split("\n"): 400 | self.headers.append(line + "\n") 401 | 402 | def __delitem__(self, name): 403 | """Delete all occurrences of a specific header, if it is present.""" 404 | name = name.lower() 405 | if not name in self.dict: 406 | return 407 | del self.dict[name] 408 | name = name + ':' 409 | n = len(name) 410 | lst = [] 411 | hit = 0 412 | for i in range(len(self.headers)): 413 | line = self.headers[i] 414 | if line[:n].lower() == name: 415 | hit = 1 416 | elif not line[:1].isspace(): 417 | hit = 0 418 | if hit: 419 | lst.append(i) 420 | for i in reversed(lst): 421 | del self.headers[i] 422 | 423 | def setdefault(self, name, default=""): 424 | lowername = name.lower() 425 | if lowername in self.dict: 426 | return self.dict[lowername] 427 | else: 428 | text = name + ": " + default 429 | for line in text.split("\n"): 430 | self.headers.append(line + "\n") 431 | self.dict[lowername] = default 432 | return default 433 | 434 | def has_key(self, name): 435 | """Determine whether a message contains the named header.""" 436 | return name.lower() in self.dict 437 | 438 | def __contains__(self, name): 439 | """Determine whether a message contains the named header.""" 440 | return name.lower() in self.dict 441 | 442 | def __iter__(self): 443 | return iter(self.dict) 444 | 445 | def keys(self): 446 | """Get all of a message's header field names.""" 447 | return list(self.dict.keys()) 448 | 449 | def values(self): 450 | """Get all of a message's header field values.""" 451 | return list(self.dict.values()) 452 | 453 | def items(self): 454 | """Get all of a message's headers. 455 | 456 | Returns a list of name, value tuples. 457 | """ 458 | return list(self.dict.items()) 459 | 460 | def __str__(self): 461 | return ''.join(self.headers) 462 | 463 | 464 | # Utility functions 465 | # ----------------- 466 | 467 | # XXX Should fix unquote() and quote() to be really conformant. 468 | # XXX The inverses of the parse functions may also be useful. 469 | 470 | 471 | def unquote(s): 472 | """Remove quotes from a string.""" 473 | if len(s) > 1: 474 | if s.startswith('"') and s.endswith('"'): 475 | return s[1:-1].replace('\\\\', '\\').replace('\\"', '"') 476 | if s.startswith('<') and s.endswith('>'): 477 | return s[1:-1] 478 | return s 479 | 480 | 481 | def quote(s): 482 | """Add quotes around a string.""" 483 | return s.replace('\\', '\\\\').replace('"', '\\"') 484 | 485 | 486 | def parseaddr(address): 487 | """Parse an address into a (realname, mailaddr) tuple.""" 488 | a = AddressList(address) 489 | lst = a.addresslist 490 | if not lst: 491 | return (None, None) 492 | return lst[0] 493 | 494 | 495 | class AddrlistClass: 496 | """Address parser class by Ben Escoto. 497 | 498 | To understand what this class does, it helps to have a copy of 499 | RFC 2822 in front of you. 500 | 501 | http://www.faqs.org/rfcs/rfc2822.html 502 | 503 | Note: this class interface is deprecated and may be removed in the future. 504 | Use rfc822.AddressList instead. 505 | """ 506 | 507 | def __init__(self, field): 508 | """Initialize a new instance. 509 | 510 | `field' is an unparsed address header field, containing one or more 511 | addresses. 512 | """ 513 | self.specials = '()<>@,:;.\"[]' 514 | self.pos = 0 515 | self.LWS = ' \t' 516 | self.CR = '\r\n' 517 | self.atomends = self.specials + self.LWS + self.CR 518 | # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it 519 | # is obsolete syntax. RFC 2822 requires that we recognize obsolete 520 | # syntax, so allow dots in phrases. 521 | self.phraseends = self.atomends.replace('.', '') 522 | self.field = field 523 | self.commentlist = [] 524 | 525 | def gotonext(self): 526 | """Parse up to the start of the next address.""" 527 | while self.pos < len(self.field): 528 | if self.field[self.pos] in self.LWS + '\n\r': 529 | self.pos = self.pos + 1 530 | elif self.field[self.pos] == '(': 531 | self.commentlist.append(self.getcomment()) 532 | else: break 533 | 534 | def getaddrlist(self): 535 | """Parse all addresses. 536 | 537 | Returns a list containing all of the addresses. 538 | """ 539 | result = [] 540 | ad = self.getaddress() 541 | while ad: 542 | result += ad 543 | ad = self.getaddress() 544 | return result 545 | 546 | def getaddress(self): 547 | """Parse the next address.""" 548 | self.commentlist = [] 549 | self.gotonext() 550 | 551 | oldpos = self.pos 552 | oldcl = self.commentlist 553 | plist = self.getphraselist() 554 | 555 | self.gotonext() 556 | returnlist = [] 557 | 558 | if self.pos >= len(self.field): 559 | # Bad email address technically, no domain. 560 | if plist: 561 | returnlist = [(' '.join(self.commentlist), plist[0])] 562 | 563 | elif self.field[self.pos] in '.@': 564 | # email address is just an addrspec 565 | # this isn't very efficient since we start over 566 | self.pos = oldpos 567 | self.commentlist = oldcl 568 | addrspec = self.getaddrspec() 569 | returnlist = [(' '.join(self.commentlist), addrspec)] 570 | 571 | elif self.field[self.pos] == ':': 572 | # address is a group 573 | returnlist = [] 574 | 575 | fieldlen = len(self.field) 576 | self.pos += 1 577 | while self.pos < len(self.field): 578 | self.gotonext() 579 | if self.pos < fieldlen and self.field[self.pos] == ';': 580 | self.pos += 1 581 | break 582 | returnlist = returnlist + self.getaddress() 583 | 584 | elif self.field[self.pos] == '<': 585 | # Address is a phrase then a route addr 586 | routeaddr = self.getrouteaddr() 587 | 588 | if self.commentlist: 589 | returnlist = [(' '.join(plist) + ' (' + \ 590 | ' '.join(self.commentlist) + ')', routeaddr)] 591 | else: returnlist = [(' '.join(plist), routeaddr)] 592 | 593 | else: 594 | if plist: 595 | returnlist = [(' '.join(self.commentlist), plist[0])] 596 | elif self.field[self.pos] in self.specials: 597 | self.pos += 1 598 | 599 | self.gotonext() 600 | if self.pos < len(self.field) and self.field[self.pos] == ',': 601 | self.pos += 1 602 | return returnlist 603 | 604 | def getrouteaddr(self): 605 | """Parse a route address (Return-path value). 606 | 607 | This method just skips all the route stuff and returns the addrspec. 608 | """ 609 | if self.field[self.pos] != '<': 610 | return 611 | 612 | expectroute = 0 613 | self.pos += 1 614 | self.gotonext() 615 | adlist = "" 616 | while self.pos < len(self.field): 617 | if expectroute: 618 | self.getdomain() 619 | expectroute = 0 620 | elif self.field[self.pos] == '>': 621 | self.pos += 1 622 | break 623 | elif self.field[self.pos] == '@': 624 | self.pos += 1 625 | expectroute = 1 626 | elif self.field[self.pos] == ':': 627 | self.pos += 1 628 | else: 629 | adlist = self.getaddrspec() 630 | self.pos += 1 631 | break 632 | self.gotonext() 633 | 634 | return adlist 635 | 636 | def getaddrspec(self): 637 | """Parse an RFC 2822 addr-spec.""" 638 | aslist = [] 639 | 640 | self.gotonext() 641 | while self.pos < len(self.field): 642 | if self.field[self.pos] == '.': 643 | aslist.append('.') 644 | self.pos += 1 645 | elif self.field[self.pos] == '"': 646 | aslist.append('"%s"' % self.getquote()) 647 | elif self.field[self.pos] in self.atomends: 648 | break 649 | else: aslist.append(self.getatom()) 650 | self.gotonext() 651 | 652 | if self.pos >= len(self.field) or self.field[self.pos] != '@': 653 | return ''.join(aslist) 654 | 655 | aslist.append('@') 656 | self.pos += 1 657 | self.gotonext() 658 | return ''.join(aslist) + self.getdomain() 659 | 660 | def getdomain(self): 661 | """Get the complete domain name from an address.""" 662 | sdlist = [] 663 | while self.pos < len(self.field): 664 | if self.field[self.pos] in self.LWS: 665 | self.pos += 1 666 | elif self.field[self.pos] == '(': 667 | self.commentlist.append(self.getcomment()) 668 | elif self.field[self.pos] == '[': 669 | sdlist.append(self.getdomainliteral()) 670 | elif self.field[self.pos] == '.': 671 | self.pos += 1 672 | sdlist.append('.') 673 | elif self.field[self.pos] in self.atomends: 674 | break 675 | else: sdlist.append(self.getatom()) 676 | return ''.join(sdlist) 677 | 678 | def getdelimited(self, beginchar, endchars, allowcomments = 1): 679 | """Parse a header fragment delimited by special characters. 680 | 681 | `beginchar' is the start character for the fragment. If self is not 682 | looking at an instance of `beginchar' then getdelimited returns the 683 | empty string. 684 | 685 | `endchars' is a sequence of allowable end-delimiting characters. 686 | Parsing stops when one of these is encountered. 687 | 688 | If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed 689 | within the parsed fragment. 690 | """ 691 | if self.field[self.pos] != beginchar: 692 | return '' 693 | 694 | slist = [''] 695 | quote = 0 696 | self.pos += 1 697 | while self.pos < len(self.field): 698 | if quote == 1: 699 | slist.append(self.field[self.pos]) 700 | quote = 0 701 | elif self.field[self.pos] in endchars: 702 | self.pos += 1 703 | break 704 | elif allowcomments and self.field[self.pos] == '(': 705 | slist.append(self.getcomment()) 706 | continue # have already advanced pos from getcomment 707 | elif self.field[self.pos] == '\\': 708 | quote = 1 709 | else: 710 | slist.append(self.field[self.pos]) 711 | self.pos += 1 712 | 713 | return ''.join(slist) 714 | 715 | def getquote(self): 716 | """Get a quote-delimited fragment from self's field.""" 717 | return self.getdelimited('"', '"\r', 0) 718 | 719 | def getcomment(self): 720 | """Get a parenthesis-delimited fragment from self's field.""" 721 | return self.getdelimited('(', ')\r', 1) 722 | 723 | def getdomainliteral(self): 724 | """Parse an RFC 2822 domain-literal.""" 725 | return '[%s]' % self.getdelimited('[', ']\r', 0) 726 | 727 | def getatom(self, atomends=None): 728 | """Parse an RFC 2822 atom. 729 | 730 | Optional atomends specifies a different set of end token delimiters 731 | (the default is to use self.atomends). This is used e.g. in 732 | getphraselist() since phrase endings must not include the `.' (which 733 | is legal in phrases).""" 734 | atomlist = [''] 735 | if atomends is None: 736 | atomends = self.atomends 737 | 738 | while self.pos < len(self.field): 739 | if self.field[self.pos] in atomends: 740 | break 741 | else: atomlist.append(self.field[self.pos]) 742 | self.pos += 1 743 | 744 | return ''.join(atomlist) 745 | 746 | def getphraselist(self): 747 | """Parse a sequence of RFC 2822 phrases. 748 | 749 | A phrase is a sequence of words, which are in turn either RFC 2822 750 | atoms or quoted-strings. Phrases are canonicalized by squeezing all 751 | runs of continuous whitespace into one space. 752 | """ 753 | plist = [] 754 | 755 | while self.pos < len(self.field): 756 | if self.field[self.pos] in self.LWS: 757 | self.pos += 1 758 | elif self.field[self.pos] == '"': 759 | plist.append(self.getquote()) 760 | elif self.field[self.pos] == '(': 761 | self.commentlist.append(self.getcomment()) 762 | elif self.field[self.pos] in self.phraseends: 763 | break 764 | else: 765 | plist.append(self.getatom(self.phraseends)) 766 | 767 | return plist 768 | 769 | class AddressList(AddrlistClass): 770 | """An AddressList encapsulates a list of parsed RFC 2822 addresses.""" 771 | def __init__(self, field): 772 | AddrlistClass.__init__(self, field) 773 | if field: 774 | self.addresslist = self.getaddrlist() 775 | else: 776 | self.addresslist = [] 777 | 778 | def __len__(self): 779 | return len(self.addresslist) 780 | 781 | def __str__(self): 782 | return ", ".join(map(dump_address_pair, self.addresslist)) 783 | 784 | def __add__(self, other): 785 | # Set union 786 | newaddr = AddressList(None) 787 | newaddr.addresslist = self.addresslist[:] 788 | for x in other.addresslist: 789 | if not x in self.addresslist: 790 | newaddr.addresslist.append(x) 791 | return newaddr 792 | 793 | def __iadd__(self, other): 794 | # Set union, in-place 795 | for x in other.addresslist: 796 | if not x in self.addresslist: 797 | self.addresslist.append(x) 798 | return self 799 | 800 | def __sub__(self, other): 801 | # Set difference 802 | newaddr = AddressList(None) 803 | for x in self.addresslist: 804 | if not x in other.addresslist: 805 | newaddr.addresslist.append(x) 806 | return newaddr 807 | 808 | def __isub__(self, other): 809 | # Set difference, in-place 810 | for x in other.addresslist: 811 | if x in self.addresslist: 812 | self.addresslist.remove(x) 813 | return self 814 | 815 | def __getitem__(self, index): 816 | # Make indexing, slices, and 'in' work 817 | return self.addresslist[index] 818 | 819 | def dump_address_pair(pair): 820 | """Dump a (name, address) pair in a canonicalized form.""" 821 | if pair[0]: 822 | return '"' + pair[0] + '" <' + pair[1] + '>' 823 | else: 824 | return pair[1] 825 | 826 | # Parse a date field 827 | 828 | _monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 829 | 'aug', 'sep', 'oct', 'nov', 'dec', 830 | 'january', 'february', 'march', 'april', 'may', 'june', 'july', 831 | 'august', 'september', 'october', 'november', 'december'] 832 | _daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] 833 | 834 | # The timezone table does not include the military time zones defined 835 | # in RFC822, other than Z. According to RFC1123, the description in 836 | # RFC822 gets the signs wrong, so we can't rely on any such time 837 | # zones. RFC1123 recommends that numeric timezone indicators be used 838 | # instead of timezone names. 839 | 840 | _timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0, 841 | 'AST': -400, 'ADT': -300, # Atlantic (used in Canada) 842 | 'EST': -500, 'EDT': -400, # Eastern 843 | 'CST': -600, 'CDT': -500, # Central 844 | 'MST': -700, 'MDT': -600, # Mountain 845 | 'PST': -800, 'PDT': -700 # Pacific 846 | } 847 | 848 | 849 | def parsedate_tz(data): 850 | """Convert a date string to a time tuple. 851 | 852 | Accounts for military timezones. 853 | """ 854 | if not data: 855 | return None 856 | data = data.split() 857 | if data[0][-1] in (',', '.') or data[0].lower() in _daynames: 858 | # There's a dayname here. Skip it 859 | del data[0] 860 | else: 861 | # no space after the "weekday,"? 862 | i = data[0].rfind(',') 863 | if i >= 0: 864 | data[0] = data[0][i+1:] 865 | if len(data) == 3: # RFC 850 date, deprecated 866 | stuff = data[0].split('-') 867 | if len(stuff) == 3: 868 | data = stuff + data[1:] 869 | if len(data) == 4: 870 | s = data[3] 871 | i = s.find('+') 872 | if i > 0: 873 | data[3:] = [s[:i], s[i+1:]] 874 | else: 875 | data.append('') # Dummy tz 876 | if len(data) < 5: 877 | return None 878 | data = data[:5] 879 | [dd, mm, yy, tm, tz] = data 880 | mm = mm.lower() 881 | if not mm in _monthnames: 882 | dd, mm = mm, dd.lower() 883 | if not mm in _monthnames: 884 | return None 885 | mm = _monthnames.index(mm)+1 886 | if mm > 12: mm = mm - 12 887 | if dd[-1] == ',': 888 | dd = dd[:-1] 889 | i = yy.find(':') 890 | if i > 0: 891 | yy, tm = tm, yy 892 | if yy[-1] == ',': 893 | yy = yy[:-1] 894 | if not yy[0].isdigit(): 895 | yy, tz = tz, yy 896 | if tm[-1] == ',': 897 | tm = tm[:-1] 898 | tm = tm.split(':') 899 | if len(tm) == 2: 900 | [thh, tmm] = tm 901 | tss = '0' 902 | elif len(tm) == 3: 903 | [thh, tmm, tss] = tm 904 | else: 905 | return None 906 | try: 907 | yy = int(yy) 908 | dd = int(dd) 909 | thh = int(thh) 910 | tmm = int(tmm) 911 | tss = int(tss) 912 | except ValueError: 913 | return None 914 | tzoffset = None 915 | tz = tz.upper() 916 | if tz in _timezones: 917 | tzoffset = _timezones[tz] 918 | else: 919 | try: 920 | tzoffset = int(tz) 921 | except ValueError: 922 | pass 923 | # Convert a timezone offset into seconds ; -0500 -> -18000 924 | if tzoffset: 925 | if tzoffset < 0: 926 | tzsign = -1 927 | tzoffset = -tzoffset 928 | else: 929 | tzsign = 1 930 | tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60) 931 | return (yy, mm, dd, thh, tmm, tss, 0, 1, 0, tzoffset) 932 | 933 | 934 | def parsedate(data): 935 | """Convert a time string to a time tuple.""" 936 | t = parsedate_tz(data) 937 | if t is None: 938 | return t 939 | return t[:9] 940 | 941 | 942 | def mktime_tz(data): 943 | """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp.""" 944 | if data[9] is None: 945 | # No zone info, so localtime is better assumption than GMT 946 | return time.mktime(data[:8] + (-1,)) 947 | else: 948 | t = time.mktime(data[:8] + (0,)) 949 | return t - data[9] - time.timezone 950 | 951 | def formatdate(timeval=None): 952 | """Returns time format preferred for Internet standards. 953 | 954 | Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 955 | 956 | According to RFC 1123, day and month names must always be in 957 | English. If not for that, this code could use strftime(). It 958 | can't because strftime() honors the locale and could generated 959 | non-English names. 960 | """ 961 | if timeval is None: 962 | timeval = time.time() 963 | timeval = time.gmtime(timeval) 964 | return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( 965 | ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[timeval[6]], 966 | timeval[2], 967 | ("Jan", "Feb", "Mar", "Apr", "May", "Jun", 968 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")[timeval[1]-1], 969 | timeval[0], timeval[3], timeval[4], timeval[5]) 970 | 971 | 972 | # When used as script, run a small test program. 973 | # The first command line argument must be a filename containing one 974 | # message in RFC-822 format. 975 | 976 | if __name__ == '__main__': 977 | import sys, os 978 | if not 'HOME' in os.environ: 979 | os.environ['HOME'] = os.path.join(os.environ['HOMEDRIVE'], \ 980 | os.environ['HOMEPATH']) 981 | file = os.path.join(os.environ['HOME'], 'Mail/inbox/1') 982 | if sys.argv[1:]: file = sys.argv[1] 983 | f = open(file, 'r') 984 | m = Message(f) 985 | print(('From:', m.getaddr('from'))) 986 | print(('To:', m.getaddrlist('to'))) 987 | print(('Subject:', m.getheader('subject'))) 988 | print(('Date:', m.getheader('date'))) 989 | date = m.getdate_tz('date') 990 | tz = date[-1] 991 | date = time.localtime(mktime_tz(date)) 992 | if date: 993 | print(('ParsedDate:', time.asctime(date),)) 994 | hhmmss = tz 995 | hhmm, ss = divmod(hhmmss, 60) 996 | hh, mm = divmod(hhmm, 60) 997 | print(("%+03d%02d" % (hh, mm),)) 998 | if ss: print((".%02d" % ss,)) 999 | print() 1000 | else: 1001 | print(('ParsedDate:', None)) 1002 | m.rewindbody() 1003 | n = 0 1004 | while f.readline(): 1005 | n += 1 1006 | print(('Lines:', n)) 1007 | print(('-'*70)) 1008 | print(('len =', len(m))) 1009 | if 'Date' in m: print(('Date =', m['Date'])) 1010 | if 'X-Nonsense' in m: pass 1011 | print(('keys =', list(m.keys()))) 1012 | print(('values =', list(m.values()))) 1013 | print(('items =', list(m.items()))) 1014 | -------------------------------------------------------------------------------- /shuffle_by_year.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Q&D script to sort mail into subfolders by year. 4 | # Reduces the burden upon the filesystem gnomes. 5 | 6 | DIRPATH = "/stor0/backups/imapbak/rtucker/Fastmail-rey_fmgirl_com" 7 | 8 | import email 9 | import mailbox 10 | import imap2maildir 11 | import sys 12 | import time 13 | import os 14 | 15 | def main(): 16 | db = imap2maildir.open_sql_session(DIRPATH + "/.imap2maildir.sqlite") 17 | mbox = mailbox.Maildir(DIRPATH, False) 18 | 19 | try: 20 | 21 | counter = 0 22 | c = db.cursor() 23 | 24 | for result in db.execute("select mailfile,folder from seenmessages where folder is null or folder = ''"): 25 | key = result[0] 26 | msg = mbox.get_message(key) 27 | 28 | year = None 29 | 30 | if 'Date' in msg: 31 | ttup = email.utils.parsedate(msg['Date']) 32 | if ttup: 33 | year = ttup[0] 34 | 35 | if year is None: 36 | tstamp = msg.get_date() 37 | year = time.gmtime(tstamp).tm_year 38 | print(key + " has no valid Date header; going with " + str(year)) 39 | 40 | ybox = mbox.add_folder(str(year)) 41 | 42 | ybox.lock() 43 | newkey = ybox.add(msg) 44 | ybox.flush() 45 | ybox.unlock() 46 | 47 | c.execute("update seenmessages set mailfile = ?, folder = ? where mailfile = ?", (newkey, year, key)) 48 | 49 | mbox.lock() 50 | mbox.discard(key) 51 | mbox.flush() 52 | mbox.unlock() 53 | 54 | print("moved " + key + " to " + str(year) + "/" + newkey) 55 | 56 | counter += 1 57 | 58 | if counter % 25 == 0: 59 | print("committing db") 60 | db.commit() 61 | sys.stdout.flush() 62 | 63 | if os.path.exists(".STOP"): 64 | print("stop requested") 65 | os.unlink(".STOP") 66 | break 67 | 68 | finally: 69 | mbox.unlock() 70 | db.commit() 71 | 72 | if __name__ == "__main__": 73 | main() 74 | 75 | -------------------------------------------------------------------------------- /simpleimap.py: -------------------------------------------------------------------------------- 1 | """ simpleimap.py, originally from http://p.linode.com/2693 on 2009/07/22 2 | Copyright (c) 2009 Timothy J Fontaine 3 | Copyright (c) 2009 Ryan S. Tucker 4 | """ 5 | 6 | import email 7 | import imaplib 8 | import logging 9 | import platform 10 | import re 11 | import time 12 | 13 | class __simplebase: 14 | """ __simple base 15 | """ 16 | 17 | def parseFetch(self, text): 18 | """Given a string (e.g. '1 (ENVELOPE...'), breaks it down into 19 | a useful format. 20 | 21 | Based on Helder Guerreiro 's 22 | imaplib2 sexp.py: http://code.google.com/p/webpymail/ 23 | """ 24 | 25 | literal_re = re.compile(r'^{(\d+)} ') 26 | simple_re = re.compile(r'^([^ ()]+)') 27 | quoted_re = re.compile(r'^"((?:[^"\\]|(?:\\\\)|\\"|\\)*)"') 28 | 29 | pos = 0 30 | length = len(text) 31 | current = '' 32 | result = [] 33 | cur_result = result 34 | level = [ cur_result ] 35 | 36 | # Scanner 37 | while pos < length: 38 | 39 | # Quoted literal: 40 | if text[pos] == '"': 41 | quoted = quoted_re.match(text[pos:]) 42 | if quoted: 43 | cur_result.append( quoted.groups()[0] ) 44 | pos += quoted.end() - 1 45 | 46 | # Numbered literal: 47 | elif text[pos] == '{': 48 | lit = literal_re.match(text[pos:]) 49 | if lit: 50 | start = pos+lit.end() 51 | end = pos+lit.end()+int(lit.groups()[0]) 52 | pos = end - 1 53 | cur_result.append( text[ start:end ] ) 54 | 55 | # Simple literal 56 | elif text[pos] not in '() ': 57 | simple = simple_re.match(text[pos:]) 58 | if simple: 59 | tmp = simple.groups()[0] 60 | if tmp.isdigit(): 61 | tmp = int(tmp) 62 | elif tmp == 'NIL': 63 | tmp = None 64 | cur_result.append( tmp ) 65 | pos += simple.end() - 1 66 | 67 | # Level handling, if we find a '(' we must add another list, if we 68 | # find a ')' we must return to the previous list. 69 | elif text[pos] == '(': 70 | cur_result.append([]) 71 | cur_result = cur_result[-1] 72 | level.append(cur_result) 73 | 74 | elif text[pos] == ')': 75 | try: 76 | cur_result = level[-2] 77 | del level[-1] 78 | except IndexError: 79 | raise ValueError('Unexpected parenthesis at pos %(pos)d text %(text)s' % {'pos':pos, 'text': text}) 80 | 81 | pos += 1 82 | 83 | # We now have a list of lists. Dict this a bit... 84 | outerdict = self.__listdictor(result) 85 | replydict = {} 86 | 87 | for i in list(outerdict.keys()): 88 | replydict[i] = self.__listdictor(outerdict[i]) 89 | 90 | return replydict 91 | 92 | def __listdictor(self, inlist): 93 | """ __listdictor 94 | """ 95 | 96 | outdict = {} 97 | 98 | for i in range(0,len(inlist),2): 99 | outdict[inlist[i]] = inlist[i+1] 100 | 101 | return outdict 102 | 103 | def parseInternalDate(self, resp): 104 | """Takes IMAP INTERNALDATE and turns it into a Python time 105 | tuple referenced to GMT. 106 | 107 | Based from: http://code.google.com/p/webpymail/ 108 | """ 109 | 110 | Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 111 | 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 112 | 113 | InternalDate = re.compile( 114 | r'(?P[ 0123]?[0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' 115 | r' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])' 116 | r' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])' 117 | ) 118 | 119 | mo = InternalDate.match(resp) 120 | if not mo: 121 | # try email date format (returns None on failure) 122 | return email.utils.parsedate(resp) 123 | 124 | mon = Mon2num[mo.group('mon')] 125 | zonen = mo.group('zonen') 126 | 127 | day = int(mo.group('day')) 128 | year = int(mo.group('year')) 129 | hour = int(mo.group('hour')) 130 | min = int(mo.group('min')) 131 | sec = int(mo.group('sec')) 132 | zoneh = int(mo.group('zoneh')) 133 | zonem = int(mo.group('zonem')) 134 | 135 | zone = (zoneh*60 + zonem)*60 136 | 137 | # handle negative offsets 138 | if zonen == '-': 139 | zone = -zone 140 | 141 | tt = (year, mon, day, hour, min, sec, -1, -1, -1) 142 | 143 | utc = time.mktime(tt) 144 | 145 | # Following is necessary because the time module has no 'mkgmtime'. 146 | # 'mktime' assumes arg in local timezone, so adds timezone/altzone. 147 | 148 | lt = time.localtime(utc) 149 | if time.daylight and lt[-1]: 150 | zone = zone + time.altzone 151 | else: 152 | zone = zone + time.timezone 153 | 154 | return time.localtime(utc - zone) 155 | 156 | def get_messages_by_folder(self, folder, charset=None, search='ALL'): 157 | """ get messages by folder 158 | """ 159 | 160 | ids = self.get_ids_by_folder(folder, charset, search) 161 | 162 | for m in self.get_messages_by_ids(ids): 163 | yield m 164 | 165 | def get_ids_by_folder(self, folder, charset=None, search='ALL'): 166 | """ get ids by folder 167 | """ 168 | 169 | self.select(folder, readonly=True) 170 | status, data = self.search(charset, search) 171 | if status != 'OK': 172 | raise Exception('search %s: %s' % (search, data[0])) 173 | 174 | return data[0].split() 175 | 176 | def get_uids_by_folder(self, folder, charset=None, search='ALL'): 177 | """ get_uids by folders 178 | """ 179 | 180 | self.select(folder, readonly=True) 181 | status, data = self.uid('SEARCH', charset, search) 182 | if status != 'OK': 183 | raise Exception('search %s: %s' % (search, data[0])) 184 | 185 | return data[0].split() 186 | 187 | def get_summaries_by_folder(self, folder, charset=None, search='ALL'): 188 | """ get summaries by folder 189 | """ 190 | 191 | for i in self.get_uids_by_folder(folder, charset, search): 192 | yield self.get_summary_by_uid(int(i)) 193 | 194 | def get_messages_by_ids(self, ids): 195 | """ get messages by ids 196 | """ 197 | 198 | for i in ids: 199 | yield self.get_message_by_id(int(i)) 200 | 201 | def get_message_by_id(self, id): 202 | """ get_message_by_id 203 | """ 204 | 205 | status, data = self.fetch(int(id), '(RFC822)') 206 | 207 | if status != 'OK': 208 | raise Exception('id %s: %s' % (uid, data[0])) 209 | 210 | return email.message_from_string(data[0][1]) 211 | 212 | def get_messages_by_uids(self, uids): 213 | """ get messages by uids 214 | """ 215 | 216 | for i in uids: 217 | yield self.get_message_by_uid(int(i)) 218 | 219 | def get_message_by_uid(self, uid): 220 | """ get_message_by_uid 221 | """ 222 | 223 | status, data = self.uid('FETCH', uid, '(RFC822)') 224 | 225 | if status != 'OK': 226 | raise Exception('uid %s: %s' % (uid, data[0])) 227 | 228 | return email.message_from_string(data[0][1]) 229 | 230 | def get_summaries_by_ids(self, ids): 231 | """ get summaries by ids 232 | """ 233 | 234 | for i in ids: 235 | yield self.get_summary_by_id(int(i)) 236 | 237 | def get_summary_by_id(self, id): 238 | """Retrieve a dictionary of simple header information for a given id. 239 | 240 | Requires: id (Sequence number of message) 241 | Returns: {'uid': UID you requested, 242 | 'msgid': RFC822 Message ID, 243 | 'size': Size of message in bytes, 244 | 'date': IMAP's Internaldate for the message, 245 | 'envelope': Envelope data} 246 | """ 247 | 248 | # Retrieve the message from the server. 249 | status, data = self.fetch(id, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 250 | 251 | if status != 'OK': 252 | return None 253 | 254 | return self.parse_summary_data(data) 255 | 256 | def get_uids_by_ids(self, ids): 257 | """ get uids by ids 258 | """ 259 | 260 | for i in ids: 261 | yield self.get_uid_by_id(int(i)) 262 | 263 | def get_uid_by_id(self, id): 264 | """Given a message number (id), returns the UID if it exists.""" 265 | status, data = self.fetch(int(id), '(UID)') 266 | 267 | if status != 'OK': 268 | raise Exception('id %s: %s' % (id, data[0])) 269 | 270 | if data[0]: 271 | uidrg = re.compile('.*?UID\\s+(\\d+)',re.IGNORECASE|re.DOTALL) 272 | uidm = uidrg.match(data[0]) 273 | if uidm: 274 | return int(uidm.group(1)) 275 | 276 | return None 277 | 278 | def get_summaries_by_uids(self, uids): 279 | """ get summaries by uids 280 | """ 281 | 282 | for i in uids: 283 | yield self.get_summary_by_uid(int(i)) 284 | 285 | def get_summary_by_uid(self, uid): 286 | """Retrieve a dictionary of simple header information for a given uid. 287 | 288 | Requires: uid (unique numeric ID of message) 289 | Returns: {'uid': UID you requested, 290 | 'msgid': RFC822 Message ID, 291 | 'size': Size of message in bytes, 292 | 'date': IMAP's Internaldate for the message, 293 | 'envelope': Envelope data} 294 | """ 295 | 296 | # Retrieve the message from the server. 297 | status, data = self.uid('FETCH', uid, 298 | '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 299 | 300 | if status != 'OK': 301 | return None 302 | 303 | return self.parse_summary_data(data) 304 | 305 | def set_seen_by_uid(self, uid): 306 | """Applies the SEEN flag to a message.""" 307 | status, data = self.uid('STORE', uid, '+FLAGS', '(\\Seen)') 308 | if status != 'OK': 309 | raise Exception('set_seen_by_uid %s: %s' % (uid, data[0])) 310 | 311 | return data[0] 312 | 313 | def parse_summary_data(self, data): 314 | """Takes the data result (second parameter) of a self.uid or 315 | self.fetch for (UID ENVELOPE RFC822.SIZE INTERNALDATE) and returns 316 | a dict of simple header information. 317 | 318 | Requires: self.uid[1] or self.fetch[1] 319 | Returns: {'uid': UID you requested, 320 | 'msgid': RFC822 Message ID, 321 | 'size': Size of message in bytes, 322 | 'date': IMAP's Internaldate for the message, 323 | 'envelope': Envelope data} 324 | """ 325 | 326 | uid = date = envdate = envfrom = msgid = size = None 327 | 328 | for idx, val in enumerate(data): 329 | if isinstance(val, tuple): 330 | # This seems to happen if there are newlines in the Subject?! 331 | data[idx] = ' '.join(list(val)) 332 | 333 | if data[0]: 334 | combined_data = ' '.join(data) 335 | 336 | # Grab a list of things in the FETCH response. 337 | fetchresult = self.parseFetch(combined_data) 338 | contents = fetchresult[list(fetchresult.keys())[0]] 339 | 340 | uid = contents['UID'] 341 | envdate = contents['ENVELOPE'][0] 342 | if 'INTERNALDATE' in contents: 343 | date = contents['INTERNALDATE'] 344 | else: 345 | date = envdate 346 | 347 | envelope = contents['ENVELOPE'] 348 | 349 | if (envelope 350 | and envelope[2] 351 | and envelope[2][0] 352 | and envelope[2][0][2] 353 | and envelope[2][0][3] 354 | ): 355 | envfrom = '@'.join(envelope[2][0][2:]) 356 | else: 357 | # No From: header. Woaaah. 358 | envfrom = 'MAILER-DAEMON' 359 | msgid = envelope[9] 360 | size = int(contents['RFC822.SIZE']) 361 | 362 | if msgid or size or date: 363 | return {'uid': int(uid), 'msgid': msgid, 'size': size, 'date': date, 'envfrom': envfrom, 'envdate': envdate} 364 | else: 365 | return None 366 | 367 | def Folder(self, folder, charset=None): 368 | """Returns an instance of FolderClass.""" 369 | return FolderClass(self, folder, charset) 370 | 371 | class FolderClass: 372 | """Class for instantiating a folder instance. 373 | 374 | TODO: Trap exceptions like: 375 | ssl.SSLError: [Errno 8] _ssl.c:1325: EOF occurred in violation of protocol 376 | by trying to reconnect to the server. 377 | (Raised up via get_summary_by_uid in Summaries when IMAP server boogers.) 378 | """ 379 | def __init__(self, parent, folder='INBOX', charset=None): 380 | self.__folder = folder 381 | self.__charset = charset 382 | self.__parent = parent 383 | self.__keepaliver = self.__keepaliver_none__ 384 | self.__turbo = None 385 | self.host = parent.host 386 | self.folder = folder 387 | 388 | def __len__(self): 389 | """ __len__ 390 | """ 391 | 392 | status, data = self.__parent.select(self.__folder, readonly=True) 393 | if status != 'OK': 394 | raise Exception('folder %s: %s' % (self.__folder, data[0])) 395 | 396 | return int(data[0]) 397 | 398 | def __keepaliver__(self, keepaliver): 399 | """ __keep aliver 400 | """ 401 | 402 | self.__keepaliver = keepaliver 403 | 404 | def __keepaliver_none__(self): 405 | """ __keepaliver_none__ 406 | """ 407 | 408 | pass 409 | 410 | def __turbo__(self, turbofunction): 411 | """Calls turbofunction(uid) for every uid, only yielding those 412 | where turbofunction returns False. Set to None to disable.""" 413 | self.__turbo = turbofunction 414 | self.__turbocounter = 0 415 | 416 | def turbocounter(self, reset=False): 417 | """ turbocounter 418 | """ 419 | 420 | if self.__turbo: 421 | oldvalue = self.__turbocounter 422 | if reset: 423 | self.__turbocounter = 0 424 | return oldvalue 425 | else: 426 | return 0 427 | 428 | def Messages(self, search='ALL'): 429 | """ Messsages 430 | """ 431 | 432 | self.__parent.select(self.__folder, readonly=True) 433 | for m in self.__parent.get_messages_by_folder(self.__folder, self.__charset, search): 434 | yield m 435 | 436 | def Summaries(self, search='ALL'): 437 | """ Summaries 438 | """ 439 | 440 | if self.__turbo: 441 | self.__parent.select(self.__folder, readonly=True) 442 | for u in self.Uids(search=search): 443 | if not self.__turbo(u): 444 | try: 445 | summ = self.__parent.get_summary_by_uid(u) 446 | if summ: 447 | yield summ 448 | except Exception: 449 | logging.exception("Couldn't retrieve uid %s", u) 450 | continue 451 | else: 452 | # long hangtimes can suck 453 | self.__keepaliver() 454 | self.__turbocounter += 1 455 | else: 456 | for s in self.__parent.get_summaries_by_folder(self.__folder, self.__charset, search): 457 | yield s 458 | 459 | def Ids(self, search='ALL'): 460 | """ Ids 461 | """ 462 | 463 | self.__parent.select(self.__folder, readonly=True) 464 | for i in self.__parent.get_ids_by_folder(self.__folder, self.__charset, search): 465 | yield i 466 | 467 | def Uids(self, search='ALL'): 468 | """ Uids 469 | """ 470 | 471 | self.__parent.select(self.__folder, readonly=True) 472 | for u in self.__parent.get_uids_by_folder(self.__folder, self.__charset, search): 473 | yield u 474 | 475 | class Server: 476 | """ Class for instantiating a server instance 477 | """ 478 | 479 | def __init__(self, hostname=None, username=None, password=None, port=None, ssl=True): 480 | """ Constructor 481 | """ 482 | 483 | self.__hostname = hostname 484 | self.__username = username 485 | self.__password = password 486 | self.__ssl = ssl 487 | self.__connection = None 488 | self.__lastnoop = 0 489 | 490 | if port: 491 | self.__port = port 492 | elif ssl: 493 | self.__port = 993 494 | else: 495 | self.__port = 143 496 | 497 | if self.__hostname and self.__username and self.__password: 498 | self.Connect() 499 | 500 | def Connect(self): 501 | """ Connect 502 | """ 503 | 504 | if self.__ssl: 505 | self.__connection = SimpleImapSSL(self.__hostname, self.__port) 506 | else: 507 | self.__connection = SimpleImap(self.__hostname, self.__port) 508 | 509 | self.__connection.login(self.__username, self.__password) 510 | 511 | def Get(self): 512 | """ Get 513 | """ 514 | 515 | return self.__connection 516 | 517 | def Keepalive(self): 518 | """Call me occasionally just to make sure everything's OK...""" 519 | if self.__lastnoop + 30 < time.time(): 520 | self.__connection.noop() 521 | self.__lastnoop = time.time() 522 | 523 | class SimpleImap(imaplib.IMAP4, __simplebase): 524 | """ Simple Imap 525 | """ 526 | 527 | pass 528 | 529 | class SimpleImapSSL(imaplib.IMAP4_SSL, __simplebase): 530 | """ Simple Imap SSL 531 | """ 532 | 533 | if platform.python_version().startswith('2.6.'): 534 | def readline(self): 535 | """Read line from remote. Overrides built-in method to fix 536 | infinite loop problem when EOF occurs, since sslobj.read 537 | returns '' on EOF.""" 538 | self.sslobj.suppress_ragged_eofs = False 539 | line = [] 540 | while 1: 541 | char = self.sslobj.read(1) 542 | line.append(char) 543 | if char == "\n": return ''.join(line) 544 | 545 | if 'Windows' in platform.platform(): 546 | def read(self, n): 547 | """Read 'size' bytes from remote. (Contains workaround)""" 548 | maxRead = 1000000 549 | # Override the read() function; fixes a problem on Windows 550 | # when it tries to eat too much. http://bugs.python.org/issue1441530 551 | if n <= maxRead: 552 | return imaplib.IMAP4_SSL.read (self, n) 553 | else: 554 | soFar = 0 555 | result = "" 556 | while soFar < n: 557 | thisFragmentSize = min(maxRead, n-soFar) 558 | fragment =\ 559 | imaplib.IMAP4_SSL.read (self, thisFragmentSize) 560 | result += fragment 561 | soFar += thisFragmentSize # only a few, so not a tragic o/head 562 | return result 563 | 564 | -------------------------------------------------------------------------------- /testsuite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ Runs various tests on stuff. 4 | """ 5 | 6 | import simpleimap 7 | import unittest 8 | 9 | 10 | class TestParseSummaryData(unittest.TestCase): 11 | """ Test Parse Summary Data 12 | """ 13 | 14 | def setUp(self): 15 | """ create an instance 16 | """ 17 | self.imap = simpleimap.SimpleImapSSL('imap.gmail.com') 18 | 19 | def testDateExchange(self): 20 | """ 21 | Tests parsing a date from some Exchange server (1 digit day). 22 | """ 23 | import time 24 | 25 | parsedtime = self.imap.parseInternalDate('1-Jul-2015 17:30:49 +0200') 26 | validtime = time.localtime(time.mktime((2015, 7, 1, 11, 30, 49, -1, -1, -1))) 27 | self.assertEqual(parsedtime, validtime) 28 | 29 | def testEmbeddedSubjectQuotes(self): 30 | """ 31 | Tests a message with embedded double quotes in the Subject. 32 | 33 | >>> imap.uid('FETCH', 57454, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 34 | """ 35 | status, data = ('OK', ['49043 (UID 57454 RFC822.SIZE 2865 INTERNALDATE "27-Mar-2007 00:51:31 +0000" ENVELOPE ("Mon, 26 Mar 2007 17:51:28 -0700" "[XXXXXXX] anybody have some RIP dates for \\"Gone, But Not Forgotten\\"" (("YyyyyyY" NIL "ZzzzzzZZzzzz" "aaaaa.bbb")) ((NIL NIL "ccccccccccccc" "dddddddddddd.eee")) (("FfffffF" NIL "GgggggGGgggg" "hhhhh.iii")) (("Jjj Kkkkkkkkk Llll" NIL "mmmmmmmmmmm" "nnnnnnnnnnn.ooo")) NIL NIL NIL "<1174956688.961513.261310@p77g2000hsh.pppppppppppp.qqq>"))']) 36 | 37 | validresult = {'uid': 57454, 'envfrom': 'ZzzzzzZZzzzz@aaaaa.bbb', 'msgid': '<1174956688.961513.261310@p77g2000hsh.pppppppppppp.qqq>', 'envdate': 'Mon, 26 Mar 2007 17:51:28 -0700', 'date': '27-Mar-2007 00:51:31 +0000', 'size': 2865} 38 | 39 | result = self.imap.parse_summary_data(data) 40 | 41 | validkeys = sorted(validresult.keys()) 42 | keys = sorted(result.keys()) 43 | 44 | self.assertEqual(validkeys, keys, "wrong keys in result") 45 | 46 | for i in validkeys: 47 | self.assertEqual(validresult[i], result[i], "mismatch on %s" % i) 48 | 49 | def testEmbeddedSubjectQuotedDoubleBackslash(self): 50 | """ 51 | Tests a message with embedded double backslash inside quotes, in the Subject. 52 | 53 | >>> imap.uid('FETCH', 120818, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 54 | 55 | From https://github.com/rtucker/imap2maildir/issues#issue/10 56 | "blablabla\\" 57 | """ 58 | status, data = ('OK', ['29 (UID 120818 RFC822.SIZE 1638 INTERNALDATE "23-Nov-2010 17:29:05 +0000" ENVELOPE ("Tue, 23 Nov 2010 12:29:01 -0500" "test message \\"blablabla\\\\\\\\\\" test message" (("aaaa bbbbbb" NIL "ccccccc" "dddddddd-eeeeeee.ffffffff.ggg")) (("aaaa bbbbbb" NIL "ccccccc" "dddddddd-eeeeeee.ffffffff.ggg")) (("aaaa bbbbbb" NIL "ccccccc" "dddddddd-eeeeeee.ffffffff.ggg")) ((NIL NIL "ccccccc" "hhhhh.iii")) NIL NIL NIL ""))']) 59 | 60 | validresult = {'uid': 120818, 'envfrom': 'ccccccc@dddddddd-eeeeeee.ffffffff.ggg', 'msgid': '', 'envdate': 'Tue, 23 Nov 2010 12:29:01 -0500', 'date': '23-Nov-2010 17:29:05 +0000', 'size': 1638} 61 | 62 | result = self.imap.parse_summary_data(data) 63 | 64 | validkeys = sorted(validresult.keys()) 65 | keys = sorted(result.keys()) 66 | 67 | self.assertEqual(validkeys, keys, "wrong keys in result") 68 | 69 | for i in validkeys: 70 | self.assertEqual(validresult[i], result[i], "mismatch on %s" % i) 71 | 72 | def testEmbeddedSubjectFiveBackslashes(self): 73 | """ 74 | Tests a message with five (!) backslashes in the Subject. 75 | >>> imap.uid('FETCH', 57455, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 76 | """ 77 | status, data = ('OK', ['49044 (UID 57455 RFC822.SIZE 554 INTERNALDATE "27-Mar-2007 01:16:26 +0000" ENVELOPE ("Mon, 26 Mar 2007 21:16:26 -0400" "s/Dicky\\\\\'s/Black Pearl Cafe/g" (("Aaaa Bbbbbb" NIL "ccccccc" "ddddd.eee")) (("Ffff Gggggg" NIL "hhhhhhh" "iiiii.jjj")) (("Kkkk Llllll" NIL "mmmmmmm" "nnnnn.ooo")) (("Pppppppp" NIL "qqqqqqqq" "rrrrrrrrrrrr.sss")) NIL NIL NIL "<4cb22bf90703261816hc33b998kad934bf355d9d737@tttt.uuuuu.vvv>"))']) 78 | 79 | validresult = {'uid': 57455, 'envfrom': 'ccccccc@ddddd.eee', 'msgid': '<4cb22bf90703261816hc33b998kad934bf355d9d737@tttt.uuuuu.vvv>', 'envdate': 'Mon, 26 Mar 2007 21:16:26 -0400', 'date': '27-Mar-2007 01:16:26 +0000', 'size': 554} 80 | 81 | result = self.imap.parse_summary_data(data) 82 | 83 | validkeys = sorted(validresult.keys()) 84 | keys = sorted(result.keys()) 85 | 86 | self.assertEqual(validkeys, keys, "wrong keys in result") 87 | 88 | for i in validkeys: 89 | self.assertEqual(validresult[i], result[i], "mismatch on %s" % i) 90 | 91 | def testInReplyTo(self): 92 | """ 93 | Test a message with an in-reply-to, for the correct message ID. 94 | >>> imap.uid('FETCH', 17264, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 95 | """ 96 | status, data = ('OK', ['17258 (UID 17264 RFC822.SIZE 3346 INTERNALDATE "20-Aug-2005 17:58:38 +0000" ENVELOPE ("Sat, 20 Aug 2005 10:58:23 -0700 (PDT)" "[AAAaaaa] Re: Talk Pages" (("Bbbbb Cccccc" NIL "ddddd" "eeeeee.fff")) ((NIL NIL "ggggggggggggg" "hhhhhhhhhhhh.iii")) ((NIL NIL "jjjjjjjjjjjjj" "kkkkkkkkkkkk.lll")) ((NIL NIL "mmmmmmmmmmmmm" "nnnnnnnnnnnn.ooo")) NIL NIL "" ""))']) 97 | 98 | validresult = {'uid': 17264, 'envfrom': 'ddddd@eeeeee.fff', 'msgid': '', 'envdate': 'Sat, 20 Aug 2005 10:58:23 -0700 (PDT)', 'date': '20-Aug-2005 17:58:38 +0000', 'size': 3346} 99 | 100 | result = self.imap.parse_summary_data(data) 101 | 102 | validkeys = sorted(validresult.keys()) 103 | keys = sorted(result.keys()) 104 | 105 | self.assertEqual(validkeys, keys, "wrong keys in result") 106 | 107 | for i in validkeys: 108 | self.assertEqual(validresult[i], result[i], "mismatch on %s" % i) 109 | 110 | def testWeirdBrokenMessage20130827(self): 111 | """ 112 | Test a message that broke something at some point... 113 | >>> imap.uid('FETCH', 447638, '(UID ENVELOPE RFC822.SIZE INTERNALDATE)') 114 | """ 115 | status, data = ('OK', [('401015 (UID 447638 RFC822.SIZE 6454 INTERNALDATE "27-Aug-2013 21:45:16 +0000" ENVELOPE ("Tue, 27 Aug 2013 15:59:36 -0600" {57}', '\n\n\n\t\taaaaaaaa bbbbbbbbbb cccc dddddd eeeeeeeeee ffffffff\n'), ' (("gggggggg" NIL "hhhhhhhh" "iiiiiiii.jjj")) (("gggggggg" NIL "hhhhhhhh" "iiiiiiii.jjj")) (("gggggggg" NIL "hhhhhhhh" "iiiiiiii.jjj")) ((NIL NIL "kkkk" "llllllll.mmm")) NIL NIL NIL "<1377640776.521d214820a42@nnnnn.ooooooooo>"))']) 116 | 117 | validresult = {'uid': 447638, 'envfrom': 'hhhhhhhh@iiiiiiii.jjj', 'msgid': '<1377640776.521d214820a42@nnnnn.ooooooooo>', 'envdate': 'Tue, 27 Aug 2013 15:59:36 -0600', 'date': '27-Aug-2013 21:45:16 +0000', 'size': 6454} 118 | 119 | result = self.imap.parse_summary_data(data) 120 | 121 | validkeys = sorted(validresult.keys()) 122 | keys = sorted(result.keys()) 123 | 124 | self.assertEqual(validkeys, keys, "wrong keys in result") 125 | 126 | for i in validkeys: 127 | self.assertEqual(validresult[i], result[i], "mismatch on %s" % i) 128 | 129 | if __name__ == '__main__': 130 | unittest.main() 131 | --------------------------------------------------------------------------------