=============
IMAP messages
=============

IMAP messages provide a convenient way to access information from a mail
message. They are roughly based on the structures that Python's email module
provides but are optimized to avoid loading too much data at once and to
provide very convenient access.

Messages are retrieved from folders which are IMessageContainers:

>>> from gocept.imapapi.account import Account
>>> account = Account('localhost', 10143, 'test', 'bsdf')
>>> INBOX = account.folders[u'INBOX']
>>> INBOX.name
u'INBOX'
>>> from gocept.imapapi.interfaces import IMessageContainer
>>> IMessageContainer.providedBy(INBOX)
True

Note that the account itself is not a message container. It only contains
folders, not messages:

>>> IMessageContainer.providedBy(account)
False

Let's get a message now:

>>> message = INBOX.messages.values()[0]
>>> message
<gocept.imapapi.message.Message object u'INBOX/...' at 0x2312872>


Headers
=======

Headers can be accessed using the dictionary API on the message. Headers are
decoded into unicode automatically:

>>> message.headers['X-IMAPAPI-Test']
u'1'
>>> message.headers['X-Correct-Encoding-Header']
u'Text \xfc'

Invalid encodings will result in unicode strings anyway but using UTF-8 and
ISO8859-1 as fallback encodings:

>>> message.headers['X-Unknown-Encoding-Header']
u'Text \xfc'
>>> message.headers['X-Wrong-Encoding-Header']
u'Text \xfc'
>>> message.headers['X-No-Encoding-Header']
u'Text \xfc or not'

Other popular headers like From, Date and Subject can be accessed by using the
dictionary API, too. They are also decoded into unicode automatically:

>>> message.headers['From']
u'test@localhost'
>>> message.headers['Date']
u'02-Jul-2008 03:05:00 +0200'
>>> message.headers['Subject']
u'Mail 1'


Raw
===

The message itself can be accessed in the raw form including all headers,
parts etc. This does not perform any mangling, decoding or other function that
would change the representation from what is stored on the server:

>>> message.raw
'From: test@localhost\r\nX-IMAPAPI-Test: 1\r\nX-No-Encoding-Header: Text \xfc or not\r\nX-Wrong-Encoding-Header: =?ascii?q?Text_=C3=BC?=\r\nX-Unknown-Encoding-Header: =?foobarschnappeldiwutz?q?Text_=C3=BC?=\r\nX-Correct-Encoding-Header: =?utf-8?q?Text_=C3=BC?=\r\nDate: 02-Jul-2008 03:05:00 +0200\r\nSubject: Mail 1\r\n\r\nEverything is ok!'

Plain text
----------

The message body itself can also be retrieved in a plain text version (which
might include encoded mime parts):

>>> print message.text
Everything is ok!

Estimating the number of attachments
------------------------------------

Messages have an attribute that holds an estimated number of attachments.
While the exact number of body parts that may be considered to be attachments
can only be determined after fetching the whole message from the server, the
``estimated_attachments`` attribute only examines the body structure which is
a lot more efficient both in terms of IMAP communication and computing effort.
However, this method cannot account for, e.g., attachments inside encrypted
parts or the question which part of a list of alternatives can actually be
displayed and thus may contribute attachments.

>>> INBOX.messages.values()[0].estimated_attachments
0
>>> INBOX.messages.values()[1].estimated_attachments
0
>>> INBOX.messages.values()[2].estimated_attachments
1
>>> INBOX.messages.values()[3].estimated_attachments
7
>>> INBOX.messages.values()[4].estimated_attachments
1
>>> INBOX.messages.values()[5].estimated_attachments
2


Body parts
==========

Body parts represent MIME sub-structures.

They can be accessed using the `body` attribute on a message:

>>> message = INBOX.messages.values()[1]
>>> message.body
<gocept.imapapi.message.BodyPart object at 0x2428811>

In messages with nested parts those are accessible using the `parts`
attribute (recursively):

>>> message.body['content_type']
'multipart/alternative'
>>> message.body.parts
[<gocept.imapapi.message.BodyPart object at 0xb78a5f6c>,
 <gocept.imapapi.message.BodyPart object at 0xb78a5fac>]
>>> message.body.parts[0]
<gocept.imapapi.message.BodyPart object at 0xb787dfec>

Retrieving a part's content
---------------------------

A part's content is accessed by `fetch`ing it:

>>> message.body.parts[0].fetch()
'Everything is ok!\r\n'
>>> message.body.parts[1].fetch()
'<html>\r\n  <head>\r\n    <title>Mail 2</title>\r\n  </head>\r\n
<body>\r\n    Everything is ok!\r\n  </body>\r\n</html>\r\n'

The transfer encoding of a message, as stored on the IMAP server, is
decoded during the fetch and works for at least for the encodings
`base64` and `quoted-printable`:

>>> message = INBOX.messages.values()[3]
>>> message.body.parts[1].parts[1]['encoding']
'base64'
>>> message.body.parts[1].parts[1].fetch()
'\xff\xd8\xff\xe0\x00\x10JFIF\x00...'

>>> message = INBOX.messages.values()[3]
>>> message.body.parts[0]['encoding']
'quoted-printable'
>>> message.body.parts[0].fetch().strip()
'das ist bestimmt der totale hass f\xfcr jeden mailer *GG...'

The `fetch` method returns a string which causes the whole body to be
loaded into memory. If you want to avoid this, you can use `fetch_file`
to store the result in a file-like object:

>>> import StringIO
>>> output = StringIO.StringIO()
>>> message.body.parts[0].fetch_file(output)
>>> output.getvalue().strip()
'das ist bestimmt der totale hass f\xfcr jeden mailer *GG...'


Header access on body parts
---------------------------

Body parts implement a partial dictionary interface to access headers.
Upper-/lowercase is normalized automatically:

>>> message = INBOX.messages.values()[1]
>>> part = message.body
>>> 'content_type' in part
True
>>> part['content_type']
'multipart/alternative'

XXX Why is this 'content_type' instead of 'content-type' and why don't we
provide normalized access?
XXX>>> 'Content_Type' in part
XXXTrue
XXX>>> part['Content_Type']
XXX'multipart/alternative'

MIME headers on body parts
--------------------------

>>> message = INBOX.messages.values()[2]
>>> part = message.body.parts[0]
>>> print part.mime_headers['Content-Disposition']
Traceback (most recent call last):
KeyError: 'Content-Disposition'
>>> print part.mime_headers.get('Content-Disposition')
None
>>> part = message.body.parts[1]
>>> part.mime_headers['Content-Disposition']
'inline'
>>> part.mime_headers.params('Content-Disposition')
{'filename': 'img.jpg'}

The body of a non-multipart message has empty MIME headers:

>>> message = INBOX.messages.values()[0]
>>> message.body.mime_headers.params('Content-Type')
{}

Non-ASCII attachment filenames should come out decoded:

>>> message = INBOX.messages.values()[4]
>>> message.body.parts[1].mime_headers.params('Content-Disposition')
{'filename': u'\xe4\xf6\xfc.png'}


Accessing parts using the Content-ID header
-------------------------------------------

Body parts can carry a `Content-ID` header (or `cid` for short) which is
globally unique and can be used to find a nested body part somewhere inside a
body part.

>>> message = INBOX.messages.values()[3]
>>> message.headers['Subject']
u'11 - AppleMail, Complex HTML with many images'

>>> part1 = message.body.by_cid('919A8163-52C0-4CE3-B45C-F05A1AE2FC3D/top.jpg')
>>> part1['id']
'<919A8163-52C0-4CE3-B45C-F05A1AE2FC3D/top.jpg>'
>>> part1['content_type']
'image/jpeg'
>>> part1['size']
27412

>>> part2 = message.body.by_cid('919A8163-52C0-4CE3-B45C-F05A1AE2FC3D/bottom.jpg')
>>> part2['id']
'<919A8163-52C0-4CE3-B45C-F05A1AE2FC3D/bottom.jpg>'
>>> part2['content_type']
'image/jpeg'
>>> part2['size']
19046


Mail flags
==========

The flags of a message are accessible through the `flags` attribute, which acts
like a set. Note that changes to the flags object propagate to the server
immediately:

>>> message.server.uid('FETCH', str(message.UID), 'FLAGS')
('OK', ['4 (UID ... FLAGS (\\Seen))'])
>>> message.flags
flags(['\\Seen'])

>>> message.flags.add(r'\Answered')
>>> message.flags
flags(['\\Answered', '\\Seen'])
>>> message.server.uid('FETCH', str(message.UID), 'FLAGS')
('OK', ['4 (UID ... FLAGS (\\Answered \\Seen))'])

>>> message.flags.remove(r'\Answered')
>>> message.flags
flags(['\\Seen'])
>>> message.server.uid('FETCH', str(message.UID), 'FLAGS')
('OK', ['4 (UID ... FLAGS (\\Seen))'])

>>> len(message.flags)
1
>>> list(message.flags)
['\\Seen']
>>> r'\Seen' in message.flags
True
>>> r'\Answered' in message.flags
False

Adding custom flags
-------------------

Messages can have non-standard flags like a marker for Junk:

>>> message.flags.add(r'$Junk')
>>> message.flags
flags(['\\Seen', '$Junk'])


Copying messages
================

Messages that already exist on the IMAP server can be copied by adding the
message object to another folder's message container.

First, create a message using a string:

>>> INBOX.messages.add('Message-ID: Foobar@localhost\n\nFoo')
>>> message = INBOX.messages.values()[-1]
>>> print message.raw
Message-ID: Foobar@localhost
<BLANKLINE>
Foo

Then, we can use the message object for adding to another folder:

>>> Bar = account.folders[u'Bar']
>>> Bar.messages.add(message)
>>> Bar.messages.values()[0].headers.get('Message-ID')
u'Foobar@localhost'


Edge cases
==========

XXX This could/should move to a regression (non-doc) test.

Fetching the body part of a `text/plain` message works:

>>> message = INBOX.messages.values()[0]
>>> message.body['content_type']
'text/plain'
>>> message.body.fetch()
'Everything is ok!'


Deleting messages
=================

Messages are deleted by removing them from their folder's messages container:

>>> del INBOX.messages[message.name]
>>> message.name in INBOX.messages
False

We can also delete multiple messages at once:

>>> len(INBOX.messages)
8
>>> keys = INBOX.messages.keys()[1:3]
>>> del INBOX.messages[1:3]
>>> len(INBOX.messages)
6
>>> for key in keys:
...     print bool(key in INBOX.messages)
False
False

Deleting all:

>>> del INBOX.messages[:]
>>> len(INBOX.messages)
0
