Working with email and mailbox using IMAP protocol.
- Parsed email message attributes
- Query builder for searching emails
- Work with emails in folders (copy, delete, flag, move, seen)
- Work with mailbox folders (list, set, get, create, exists, rename, delete, status)
- No dependencies
Python version | 3.3+ |
License | Apache-2.0 |
PyPI | https://pypi.python.org/pypi/imap_tools/ |
IMAP | VERSION 4rev1 - https://tools.ietf.org/html/rfc3501 |
Contents
$ pip install imap_tools
from imap_tools import MailBox, Q
# get list of email subjects from INBOX folder
with MailBox('imap.mail.com').login('test@mail.com', 'password') as mailbox:
subjects = [msg.subject for msg in mailbox.fetch()]
# get list of email subjects from INBOX folder - equivalent verbose version
mailbox = MailBox('imap.mail.com')
mailbox.login('test@mail.com', 'password', initial_folder='INBOX') # or mailbox.folder.set instead 3d arg
subjects = [msg.subject for msg in mailbox.fetch(Q(all=True))]
mailbox.logout()
MailBox.fetch - email message generator, first searches email ids by criteria, then fetch and yields emails by one:
- criteria = 'ALL' message search criteria, docs
- 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"
- miss_defect = True, miss emails with defects
- miss_no_uid = True, miss emails without uid
- 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)
MailBox.box - imaplib.IMAP4/IMAP4_SSL client instance.
Message and Attachment public attributes are cached by functools.lru_cache
for message in mailbox.fetch():
message.uid # str or None: '123'
message.subject # str: 'some subject'
message.from_ # str: 'sender@ya.ru'
message.to # tuple: ('iam@goo.ru', 'friend@ya.ru', )
message.cc # tuple: ('cc@mail.ru', )
message.bcc # tuple: ('bcc@mail.ru', )
message.reply_to # tuple: ('reply_to@mail.ru', )
message.date # datetime.datetime: 1900-1-1 for unparsed, may be naive or with tzinfo
message.date_str # str: original date - 'Tue, 03 Jan 2017 22:26:59 +0500'
message.text # str: 'hi'
message.html # str: '<b>hi</b>'
message.flags # tuple: ('SEEN', 'FLAGGED', 'ENCRYPTED')
message.headers # dict: {'Received': ('from 1.m.ru', 'from 2.m.ru'), 'AntiVirus': ('Clean',)}
for att in message.attachments: # list: [Attachment objects]
att.filename # str: 'cat.jpg'
att.content_type # str: 'image/jpeg'
att.payload # bytes: b'\xff\xd8\xff\xe0\'
message.obj # email.message.Message: original object
message.from_values # dict or None: {'email': 'im@ya.ru', 'name': 'Ya 你', 'full': 'Ya 你 <im@ya.ru>'}
message.to_values # tuple: ({'email': '', 'name': '', 'full': ''},)
message.cc_values # tuple: ({'email': '', 'name': '', 'full': ''},)
message.bcc_values # tuple: ({'email': '', 'name': '', 'full': ''},)
message.reply_to_values # tuple: ({'email': '', 'name': '', 'full': ''},)
Possible search approaches:
from imap_tools import Q, AND, OR, NOT
mailbox.fetch(Q(subject='weather')) # query, the str-like object - see below
mailbox.fetch('TEXT "hello"') # str, use charset arg for non US-ASCII chars
mailbox.fetch(b'TEXT "\xd1\x8f"') # bytes, charset arg is ignored
Implemented query builder for search logic described in rfc3501. See query examples.
- Class AND and its alias Q are used to combine keys by the logical "and" condition.
- Class OR is used to combine keys by the logical "or" condition.
- Class NOT is used to invert the result of a logical expression.
- Class H (Header) is used to search by headers.
If the "charset" argument is specified in MailBox.fetch, the search string will be encoded to this encoding. You can change this behavior by overriding MailBox._criteria_encoder or pass criteria as bytes in desired encoding.
from imap_tools import Q, AND, OR, NOT
# AND
Q(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
Q(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='to@ya.ru')
# encoding
mailbox.fetch(Q(subject='привет'), charset='utf8') # 'привет' will be encoded by MailBox._criteria_encoder
# python note: you can't do: Q(text='two', NOT(subject='one'))
Q(NOT(subject='one'), text='two') # use kwargs after logic classes
The search key types are 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 "from@ya.ru" | contain specified str in envelope struct's FROM field |
to | str* | TO "to@ya.ru" | 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 "bcc@ya.ru" | contain specified str in envelope struct's BCC field |
cc | str* | CC "cc@ya.ru" | 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 | 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 "some_lbl" | have this gmail label. |
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.
You can use 2 approaches to perform these operations:
- "in bulk" - Perform IMAP operation for message set per 1 command
- "by one" - Perform IMAP operation for each message separately per N commands
Result of MailBox.fetch generator in actions will be implicitly converted to uid list.
For actions with a large number of messages imap command may be too large and will throw an exception, use 'limit' argument for fetch in this case.
with MailBox('imap.mail.com').login('test@mail.com', 'pwd', initial_folder='INBOX') as mailbox:
# COPY all messages from current folder to folder1, *by one
for msg in mailbox.fetch():
res = mailbox.copy(msg.uid, 'INBOX/folder1')
# MOVE all messages from current folder to folder2, *in bulk (implicit creation of uid list)
mailbox.move(mailbox.fetch(), 'INBOX/folder2')
# DELETE all messages from current folder, *in bulk (explicit creation of uid list)
mailbox.delete([msg.uid for msg in mailbox.fetch()])
# FLAG unseen messages in current folder as Answered and Flagged, *in bulk.
flags = (imap_tools.MessageFlags.ANSWERED, imap_tools.MessageFlags.FLAGGED)
mailbox.flag(mailbox.fetch('(UNSEEN)'), flags, True)
# SEEN: mark all messages sent at 05.03.2007 in current folder as unseen, *in bulk
mailbox.seen(mailbox.fetch("SENTON 05-Mar-2007"), False)
with MailBox('imap.mail.com').login('test@mail.com', 'pwd') as mailbox:
# LIST
for folder_info in mailbox.folder.list('INBOX'):
print(folder_info) # {'name': 'INBOX|cats', 'delim': '|', 'flags': ('\\Unmarked', '\\HasChildren')}
# SET
mailbox.folder.set('INBOX')
# GET
current_folder = mailbox.folder.get()
# CREATE
mailbox.folder.create('folder1')
# EXISTS
is_exists = mailbox.folder.exists('folder1')
# RENAME
mailbox.folder.rename('folder1', 'folder2')
# DELETE
mailbox.folder.delete('folder2')
# STATUS
folder_status = mailbox.folder.status('some_folder')
print(folder_status) # {'MESSAGES': 41, 'RECENT': 0, 'UIDNEXT': 11996, 'UIDVALIDITY': 1, 'UNSEEN': 5}
- Excessive low level of imaplib library.
- Other libraries contain various shortcomings or not convenient.
- Open source projects makes world better.
release_notes.rst
If you found a bug or have a question, please let me know - create merge request or issue.
Thanks to: