High level lib for work with email by IMAP:
- Basic message operations: fetch, uids, numbers
- Parsed email message attributes
- Query builder for search criteria
- Actions with emails: copy, delete, flag, move, append
- Actions with folders: list, set, get, create, exists, rename, subscribe, delete, status
- IDLE commands: start, poll, stop, wait
- Exceptions on failed IMAP operations
- No external dependencies, tested
Python version | 3.5+ |
License | Apache-2.0 |
PyPI | https://pypi.python.org/pypi/imap_tools/ |
RFC | IMAP4.1, EMAIL, IMAP related RFCs |
Repo mirror | https://gitflic.ru/project/ikvk/imap-tools |
Contents
$ pip install imap-tools
Info about lib are at: this page, docstrings, issues, pull requests, examples, source, stackoverflow.com
from imap_tools import MailBox, AND
# Get date, subject and body len of all emails from INBOX folder
with MailBox('imap.mail.com').login('[email protected]', 'pwd') as mailbox:
for msg in mailbox.fetch():
print(msg.date, msg.subject, len(msg.text or msg.html))
MailBox, MailBoxTls, MailBoxUnencrypted
- for create mailbox client. TLS example.
BaseMailBox.<auth>
- login, login_utf8, xoauth2, logout - authentication functions, they support context manager.
BaseMailBox.fetch
- first searches email uids by criteria in current folder, then fetch and yields MailMessage, args:
- criteria = 'ALL', message search criteria, query builder
- charset = 'US-ASCII', indicates charset of the strings that appear in the search criteria. See rfc2978
- limit = None, limit on the number of read emails, useful for actions with a large number of messages, like "move". May be int or slice.
- mark_seen = True, mark emails as seen on fetch
- reverse = False, in order from the larger date to the smaller
- headers_only = False, get only email headers (without text, html, attachments)
- bulk = False, False - fetch each message separately per N commands - low memory consumption, slow; True - fetch all messages per 1 command - high memory consumption, fast; int - fetch all messages by bulks of the specified size, for 20 messages and bulk=5 -> 4 commands
- sort = None, criteria for sort messages on server, use SortCriteria constants. Charset arg is important for sort
BaseMailBox.uids
- search mailbox for matching message uids in current folder, returns [str | None], None when MailMessage.from_bytes used, args:
- criteria = 'ALL', message search criteria, query builder
- charset = 'US-ASCII', indicates charset of the strings that appear in the search criteria. See rfc2978
- sort = None, criteria for sort messages on server, use SortCriteria constants. Charset arg is important for sort
BaseMailBox.<action>
- copy, move, delete, flag, append
BaseMailBox.folder
- folder manager
BaseMailBox.idle
- idle manager
BaseMailBox.numbers
- search mailbox for matching message numbers in current folder, returns [str]
BaseMailBox.numbers_to_uids
- Get message uids by message numbers, returns [str]
BaseMailBox.client
- imaplib.IMAP4/IMAP4_SSL client instance.
Email has 2 basic body variants: text and html. Sender can choose to include: one, other, both or neither(rare).
MailMessage and MailAttachment public attributes are cached by functools.lru_cache
for msg in mailbox.fetch(): # generator: imap_tools.MailMessage
msg.uid # str | None: '123'
msg.subject # str: 'some subject 你 привет'
msg.from_ # str: 'Bartö[email protected]'
msg.to # tuple: ('[email protected]', '[email protected]', )
msg.cc # tuple: ('[email protected]', )
msg.bcc # tuple: ('[email protected]', )
msg.reply_to # tuple: ('[email protected]', )
msg.date # datetime.datetime: 1900-1-1 for unparsed, may be naive or with tzinfo
msg.date_str # str: original date - 'Tue, 03 Jan 2017 22:26:59 +0500'
msg.text # str: 'Hello 你 Привет'
msg.html # str: '<b>Hello 你 Привет</b>'
msg.flags # tuple: ('\\Seen', '\\Flagged', 'ENCRYPTED')
msg.headers # dict: {'received': ('from 1.m.ru', 'from 2.m.ru'), 'anti-virus': ('Clean',)}
msg.size_rfc822 # int: 20664 bytes - size info from server (*useful with headers_only arg)
msg.size # int: 20377 bytes - size of received message
for att in msg.attachments: # list: imap_tools.MailAttachment
att.filename # str: 'cat.jpg'
att.payload # bytes: b'\xff\xd8\xff\xe0\'
att.content_id # str: '[email protected]'
att.content_type # str: 'image/jpeg'
att.content_disposition # str: 'inline'
att.part # email.message.Message: original object
att.size # int: 17361 bytes
msg.obj # email.message.Message: original object
msg.from_values # imap_tools.EmailAddress | None
msg.to_values # tuple: (imap_tools.EmailAddress,)
msg.cc_values # tuple: (imap_tools.EmailAddress,)
msg.bcc_values # tuple: (imap_tools.EmailAddress,)
msg.reply_to_values # tuple: (imap_tools.EmailAddress,)
# imap_tools.EmailAddress example:
# EmailAddress(name='Ya', email='[email protected]') # has "full" property = 'Ya <[email protected]>'
The "criteria" argument is used at fetch, uids, numbers methods of MailBox. Criteria can be of three types:
from imap_tools import AND
mailbox.fetch(AND(subject='weather')) # query, the str-like object
mailbox.fetch('TEXT "hello"') # str
mailbox.fetch(b'TEXT "\xd1\x8f"') # bytes
Use "charset" argument for encode criteria to the desired encoding. If criteria is bytes - encoding will be ignored.
mailbox.uids(A(subject='жёлтый'), charset='utf8')
Query builder implements all search logic described in rfc3501. It uses this classes:
Class | Alias | Description | Arguments |
---|---|---|---|
AND | A | Combine conditions by logical "AND" | Search keys (see table below) | str |
OR | O | Combine conditions by logical "OR" | Search keys (see table below) | str |
NOT | N | Invert the result of a logical expression | AND/OR instances | str |
Header | H | Header value for search by header key | name: str, value: str |
UidRange | U | UID range value for search by uid key | start: str, end: str |
See query examples. A few examples:
from imap_tools import A, AND, OR, NOT
# AND
A(text='hello', new=True) # '(TEXT "hello" NEW)'
# OR
OR(text='hello', date=datetime.date(2000, 3, 15)) # '(OR TEXT "hello" ON 15-Mar-2000)'
# NOT
NOT(text='hello', new=True) # 'NOT (TEXT "hello" NEW)'
# complex
A(OR(from_='[email protected]', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='[email protected]')
# python note: you can't do: A(text='two', NOT(subject='one'))
A(NOT(subject='one'), text='two') # use kwargs after logic classes (args)
Server side search notes:
- For string search keys a message matches if the string is a substring of the field. The matching is case-insensitive.
- When searching by dates - email's time and timezone are disregarding.
Search key table. Key types marked with * can accepts a sequence of values like list, tuple, set or generator.
Key | Types | Results | Description |
---|---|---|---|
answered | bool | ANSWERED/UNANSWERED | with/without the Answered flag |
seen | bool | SEEN/UNSEEN | with/without the Seen flag |
flagged | bool | FLAGGED/UNFLAGGED | with/without the Flagged flag |
draft | bool | DRAFT/UNDRAFT | with/without the Draft flag |
deleted | bool | DELETED/UNDELETED | with/without the Deleted flag |
keyword | str* | KEYWORD KEY | with the specified keyword flag |
no_keyword | str* | UNKEYWORD KEY | without the specified keyword flag |
from_ | str* | FROM "[email protected]" | contain specified str in envelope struct's FROM field |
to | str* | TO "[email protected]" | contain specified str in envelope struct's TO field |
subject | str* | SUBJECT "hello" | contain specified str in envelope struct's SUBJECT field |
body | str* | BODY "some_key" | contain specified str in body of the message |
text | str* | TEXT "some_key" | contain specified str in header or body of the message |
bcc | str* | BCC "[email protected]" | contain specified str in envelope struct's BCC field |
cc | str* | CC "[email protected]" | contain specified str in envelope struct's CC field |
date | datetime.date* | ON 15-Mar-2000 | internal date is within specified date |
date_gte | datetime.date* | SINCE 15-Mar-2000 | internal date is within or later than the specified date |
date_lt | datetime.date* | BEFORE 15-Mar-2000 | internal date is earlier than the specified date |
sent_date | datetime.date* | SENTON 15-Mar-2000 | rfc2822 Date: header is within the specified date |
sent_date_gte | datetime.date* | SENTSINCE 15-Mar-2000 | rfc2822 Date: header is within or later than the specified date |
sent_date_lt | datetime.date* | SENTBEFORE 1-Mar-2000 | rfc2822 Date: header is earlier than the specified date |
size_gt | int >= 0 | LARGER 1024 | rfc2822 size larger than specified number of octets |
size_lt | int >= 0 | SMALLER 512 | rfc2822 size smaller than specified number of octets |
new | True | NEW | have the Recent flag set but not the Seen flag |
old | True | OLD | do not have the Recent flag set |
recent | True | RECENT | have the Recent flag set |
all | True | ALL | all, criteria by default |
uid | iter(str)/str/U | UID 1,2,17 | corresponding to the specified unique identifier set |
header | H(str, str)* | HEADER "A-Spam" "5.8" | have a header that contains the specified str in the text |
gmail_label | str* | X-GM-LABELS "label1" | have this gmail label |
First of all read about UID at rfc3501.
Action's uid_list arg may takes:
- str, that is comma separated uids
- Sequence, that contains str uids
To get uids, use the maibox methods: uids, fetch.
For actions with a large number of messages imap command may be too large and will cause exception at server side, use 'limit' argument for fetch in this case.
with MailBox('imap.mail.com').login('[email protected]', 'pwd', initial_folder='INBOX') as mailbox:
# COPY messages with uid in 23,27 from current folder to folder1
mailbox.copy('23,27', 'folder1')
# MOVE all messages from current folder to INBOX/folder2
mailbox.move(mailbox.uids(), 'INBOX/folder2')
# DELETE messages with 'cat' word in its html from current folder
mailbox.delete([msg.uid for msg in mailbox.fetch() if 'cat' in msg.html])
# FLAG unseen messages in current folder as \Seen, \Flagged and TAG1
flags = (imap_tools.MailMessageFlags.SEEN, imap_tools.MailMessageFlags.FLAGGED, 'TAG1')
mailbox.flag(mailbox.uids(AND(seen=False)), flags, True)
# APPEND: add message to mailbox directly, to INBOX folder with \Seen flag and now date
with open('/tmp/message.eml', 'rb') as f:
msg = imap_tools.MailMessage.from_bytes(f.read()) # *or use bytes instead MailMessage
mailbox.append(msg, 'INBOX', dt=None, flag_set=[imap_tools.MailMessageFlags.SEEN])
BaseMailBox.login/xoauth2 has initial_folder arg, that is "INBOX" by default, use None for not set folder on login.
with MailBox('imap.mail.com').login('[email protected]', 'pwd') as mailbox:
# LIST: get all subfolders of the specified folder (root by default)
for f in mailbox.folder.list('INBOX'):
print(f) # FolderInfo(name='INBOX|cats', delim='|', flags=('\\Unmarked', '\\HasChildren'))
# SET: select folder for work
mailbox.folder.set('INBOX')
# GET: get selected folder
current_folder = mailbox.folder.get()
# CREATE: create new folder
mailbox.folder.create('INBOX|folder1')
# EXISTS: check is folder exists (shortcut for list)
is_exists = mailbox.folder.exists('INBOX|folder1')
# RENAME: set new name to folder
mailbox.folder.rename('folder3', 'folder4')
# SUBSCRIBE: subscribe/unsubscribe to folder
mailbox.folder.subscribe('INBOX|папка два', True)
# DELETE: delete folder
mailbox.folder.delete('folder4')
# STATUS: get folder status info
stat = mailbox.folder.status('some_folder')
print(stat) # {'MESSAGES': 41, 'RECENT': 0, 'UIDNEXT': 11996, 'UIDVALIDITY': 1, 'UNSEEN': 5}
IDLE logic are in mailbox.idle manager, its methods are in the table below:
Method | Description | Arguments |
---|---|---|
start | Switch on mailbox IDLE mode | |
poll | Poll for IDLE responses | timeout: Optional[float] |
stop | Switch off mailbox IDLE mode | |
wait | Switch on IDLE, poll responses, switch off IDLE on response, return responses | timeout: Optional[float] |
from imap_tools import MailBox, A
# waiting for updates 60 sec, print unseen immediately if any update
with MailBox('imap.my.moon').login('acc', 'pwd', 'INBOX') as mailbox:
responses = mailbox.idle.wait(timeout=60)
if responses:
for msg in mailbox.fetch(A(seen=False)):
print(msg.date, msg.subject)
else:
print('no updates in 60 sec')
Read docstrings and see detailed examples.
Most lib server actions raises exception if result is marked as not success.
Custom lib exceptions here: errors.py.
History of important changes: release_notes.rst
If you found a bug or have a question, then:
- Look for answer at: this page, issues, pull requests, examples, source, RFCs, stackoverflow.com, internet.
- And only then - create merge request or issue.
- Excessive low level of imaplib library.
- Other libraries contain various shortcomings or not convenient.
- Open source projects make world better.
Big thanks to people who helped develop this library:
shilkazx, somepad, 0xThiebaut, TpyoKnig, parchd-1, dojasoncom, RandomStrangerOnTheInternet, jonnyarnold, Mitrich3000, audemed44, mkalioby, atlas0fd00m, unqx, daitangio, upils, Foosec, frispete, PH89, amarkham09, nixCodeX, backelj, ohayak, mwherman95926, andyfensham, mike-code, aknrdureegaesr, ktulinger, SamGenTLEManKaka, devkral, tnusraddinov, thepeshka, shofstet, the7erm, c0da, dev4max, ascheucher, Borutia, nathan30, daniel55411, rcarmo, bhernacki, ilep, ThKue, repodiac, tiuub, Yannik, pete312, edkedk99, UlisseMini, Nicarex, RanjithNair1980, NickC-NZ, mweinelt, lucbouge, JacquelinCharbonnel, stumpylog, dimitrisstr, abionics, link2xt, Docpart, meetttttt, sapristi, thomwiggers, histogal, K900, homoLudenus, sphh
- Found a bug or figure out how to improve the library - open issue or merge request 🎯
- Do not know how to improve library - try to help other open projects that you use ✋
- Nowhere to put your money - spend it on your family, friends, loved ones, or people around you 💰
- Star the project ⭐