3. Getting started¶
3.1. Starting a session¶
The first thing you do in a MAPI application is to log on to MAPI, thereby creating a session that you can use to communicate with the server. The session itself represents one user logged on to the server.
In windows, you can create profile via the MAPI profile editor. In linux this component is not available, and therefore you must create a profile before logging on.
The profile contains information like the type of service (Kopano in this case), and variables like username, password, path, proxy options, etc. If you have an existing profile, it’s easy to log on to such a profile:
import MAPI session = MAPILogonEx(0, 'profilename', None, 0)
We simply pass the profile name and password (None in this case) and MAPI gives us a session object.
If you try this in linux, it will always fail; this is because linux does not keep a central log of profiles - each application starts fresh with no profiles. This means that you have to create a profile first. Since this is not a very simple operation, MAPI.Util provides us with a function to create a temporary profile, logon, and then delete the temporary profile:
import MAPI from MAPI.Util import * session = OpenECSession('username', 'password', 'http://localhost:236/kopano')
This will work in both win32 and linux environments and will allow you to log on via HTTP, HTTPS or FILE (unix socket) connections.
If your application is running as a trusted user (see local_admin_users setting in kopano’s server.cfg), then you can use the unix socket file:///var/run/kopano/server.sock to connect with kopano without specifying a password. This allows you to run a task as the target user without knowing the user’s password.
3.2. Opening a store¶
So now we have a session, it’s time to open a store. A store is basically an entire tree of folders that the user can use. Normally, each new profile has two stores: the user’s own store, and the public store. Most applications will want to open the user’s own store, known as the default store. Again, MAPI.Util provides a convenient function to do this, GetDefaultStore().
store = GetDefaultStore(session)
Now we have a store. The store has a set of properties like the name of the store and its unique identifier. The store object itself is just a reference to the C++ object beneath so to get properties we have to use the GetProps() method to get information:
from MAPI.Tags import * props = store.GetProps([PR_DISPLAY_NAME], 0)
You can see here that we need to import another module, MAPI.Tags. This module contains the symbolic names of all of the standard MAPI properties, like PR_DISPLAY_NAME. In reality, PR_DISPLAY_NAME is just an int specifying a property ID (a 16-bit integer identifier) and a type (also a 16-bit identifier). In this case, PR_DISPLAY_NAME is equal to 0x3001001e, with 0x3001 being the ID part, and 0x001e being the type (PT_STRING8).
GetProps() accepts a list of properties, but since we only want one for now, that’s fine. Now we have the property, we can print the value:
print props.Value + '\n'
The returned value from GetProps() is a list of SPropValue() class instances, which is a simply object containing the two values of the C++ SPropValue structure:
- ulPropTag: The property tag of the property
- Value: The value of the property (can be int, string, double, MAPI.Util.FileTime)
The returned value in ulPropTag is the same as the requested property, PR_DISPLAY_NAME. This may seem redundant, but as we will see later, it can in some cases be different from the requested property (usually in error conditions).
The number of items in the list returned by GetProps() will always be exactly equal to the number of requested properties in the GetProps() call. You can also pass None as the property list, which will cause GetProps() to return all the properties in the object.
3.3. Opening the inbox¶
We said before that the store is basically a set of folders in a hierarchy. There is a function that will give us the unique identifier of the inbox: GetReceiveFolder():
result = store.GetReceiveFolder('IPM', 0) inboxid = result
The GetReceiveFolder() function actually returns two values: the unique id of the requested message class (IPM) and the message class that actually matched (IPM). There is not much point in explaining the exact meaning of these message classes since there is hardly ever any reason not to use IPM as the message class when you pass it to GetReceiveFolder().
Once you have that unique ID, you can use the generic OpenEntry() method of the store to open the object with that unique ID. In fact, OpenEntry() can open both folders and messages, purely depending on which unique identifier you request:
inbox = store.OpenEntry(inboxid, None, 0)
Now we have an open inbox object. Just like stores, an inbox also has a PR_DISPLAY_NAME property. You can get it in just the same way as the store:
props = inbox.GetProps([PR_DISPLAY_NAME], 0) print props.Value + '\n'
This will show Inbox or your localized version of Inbox.
3.4. Using tables: listing messages¶
Most of the data inside MAPI can be viewed by using MAPI tables. MAPI tables are just what you would expect: a column/row view of a set of data. In this case, the table we want is the table of e-mail messages in the inbox. Each message in the inbox is one row in the table, and the columns are the properties of each message, for example the subject and the data.
The best way to think of the inbox’s table is exactly the way that it is shown in your e-mail client: all the emails vertically and various properties of the emails horizontally.
Tables in MAPI are very flexible and allow setting different columns, they have a row cursor, allow reading arbitrary numbers of rows, support sorting and even grouping.
We can use a MAPI table to read the messages in the inbox (in fact, this is the only way to get the list of messages in the inbox):
table = inbox.GetContentsTable(0) table.SetColumns([PR_ENTRYID, PR_SUBJECT], 0) table.SortTable(SSortOrderSet( [ SSort(PR_SUBJECT, TABLE_SORT_ASCEND) ], 0, 0), 0); rows = table.QueryRows(10, 0)
This needs some more explanation. The first line calls GetContentsTable() to get the table associated with the inbox. The SetColumns() call then selects two columns that we are interested in:
- PR_ENTRYID (the unique ID of each message) and
- PR_SUBJECT (the subject of each message)
We could have requested much more properties, but requesting more properties is slower because it needs more bandwidth, i/o, cache, etc. from the server.
We then sort the table by creating an SSortOrderSet. The SSortOrderSet constructor accepts a list of the columns to sort by (note that the columns used for sorted are not necessarily in the viewable column set set by SetColumns()). Each property can be sorted ascending (TABLE_SORT_ASCEND) or descending (TABLE_SORT_DESCEND). The two zero’s passed to the SSortOrderSet constructor are how many columns you wish to use for grouping, and how many of those groups should be expanded, but we’re not using that right now.
The last line actually requests the data from the server. The QueryRows() call requests 10 rows. Since we just openen the table, it will give us the first 10 rows. A subsequent call to QueryRows() will give us the next 10 rows, etc.
The rows are returned as a two-dimensional array: an array of arrays of SPropValues. Processing them gives you an idea of what it looks like:
for row in rows: print row.Value + '\n'
It is important to note that the order of the properties in the row output is always exactly equal to the order of properties passed to SetColumns(). In this case we know that row is the PR_SUBJECT property, and we can therefore print it in the way described above.
3.5. Opening a message¶
The table described above is great for getting information for lots of e-mails at once, but it has some (designed) limitations:
- You can only see properties of the email themselves, not the attachments or recipients of the e-mails
- Strings are truncated at 255 bytes
The reason for this is that tables are designed for summary overviews of e-mails, not for showing the details of a single message (eg when a user clicks on a message)
To get the message details, we need to open the message itself. We said before that we can use the OpenEntry() method of the store to open both folders and messages, and that’s just what we’re going to do now:
for row in rows: message = store.OpenEntry(row.Value, None, 0) props = message.GetProps(PR_SUBJECT) print props.Value + '\n'
This will open each message, and then use the GetProps() method again to get the PR_SUBJECT property. This is a little redundant since we already read the PR_SUBJECT from the table before. The only difference between this PR_SUBJECT and the PR_SUBJECT retrieved from the table is that this PR_SUBJECT is not truncated at 255 bytes.
We can now use the message object to do more interesting things.
3.6. Getting attachments¶
Each message (email) has a list of attachments. To get the list of attachments for an email, we can use the GetAttachmentTable() method on the message. As the name implies, this will return a MAPI table, exactly like the one we used before, but this time the rows will be the attachments, and the columns will be the properties (like before)
t = message.GetAttachmentTable(0) t.SetColumns([PR_ATTACH_FILENAME, PR_ATTACH_NUM], 0) rows = t.QueryRows(10,0)
As before, we can read the attachments by querying a certain column set, and then reading the rows with QueryRows(). The PR_ATTACH_NUM column can be used to open the actual attachment:
attach = message.OpenAttach(rows.Value, None, 0) props = attach.GetProps([PR_ATTACH_FILENAME], 0)
Again, the attachment object itself can be queried with GetProps() to retrieve properties.
The data inside the attachment is just another property: PR_ATTACH_DATA_BIN. Logically you’d expect to be able to call GetProps() passing that property tag (PR_ATTACH_DATA_BIN) to get the attachment data. However, this would be a bad idea - if the attachment is big, you’d get a huge SPropValue value with several megabytes of data in the Value property of the SPropValue object.
To avoid this, MAPI doesn’t allow you to get more than 8k of data via the GetProps() interface. If you have any more than that, you will get an error. Not just any error though, the GetProps() call will succeed as usual. However, the SPropValue returned will contain special information:
- ulPropTag will contain the same property ID, but the property type will be PT_ERROR. eg. If you request PR_SUBJECT (0x0037001e), and it is not available for any reason, you will get 0x0037000a (note that only the lower 16 bit, the property type, are different)
- Value will contain the MAPI error value. When the value was not returning because it was larger than 8k, the error will be MAPI_E_NOT_ENOUGH_MEMORY, if the property was not found at all, the error value will be MAPI_E_NOT_FOUND
To get the data, we have to use a stream. The MAPI stream is just like any other stream, allowing read/write/seek access to a sequential stream of data. We open the stream with the OpenProperty() call:
import MAPI.Guid stream = attach.OpenProperty(PR_ATTACH_DATA_BIN, IID_IStream, 0, 0)
The OpenProperty() method takes the property tag you wish to open, an interface identifier (in this case we want to open a stream), and some flags.
We can then use the stream to read the attachment data in 4k blocks:
data = '' while True: d = stream.Read(4096) if len(d) == 0: break data += d
This negates the effect of not having all that data in memory, but it illustrates the use of the stream pretty well.
3.7. Writing data¶
Until now we have only been reading data from MAPI. The most important ways of writing data to MAPI are:
- message.SetProps(): the opposite of GetProps()
- message.SaveChanges(): save changes done with GetProps()
- message.CreateAttach(): create a new attachment
- folder.CreateMessage(): create a new message
However, if you were to simply take the code we have created until now and added any of these functions, an exception would be raised. This is because all the flags we have been passing were 0, and most functions default to read-only access. The calls we have to change are:
- OpenEntry(entryid, None, MAPI_MODIFY) : Open a message or folder with write permissions
- OpenProperty(proptag, IID_IStream, 0, MAPI_MODIFY): Open an existing stream with write permissions
- OpenProperty(proptag, IID_IStream, 0, MAPI_MODIFY|MAPI_CREATE): Open a new stream with write permissions, or truncate the existing stream
This allows you to write to the objects:
message.SetProps([SPropValue(PR_SUBJECT, 'new subject')]) stream.Write('hello')
Although changes on messages are not saved to disk until SaveChanges() is called on messages, changes on folders and stores are immediate and do not require a SaveChanges() call.
Recipients, like attachments, can be accessed through a table. You can open a message’s recipient table with the GetRecipientTable() method. Again, the columns are properties, but this time the rows are the recipients. Each recipient has a PR_DISPLAY_NAME (eg Joe Jones), a PR_EMAIL_ADDRESS (eg firstname.lastname@example.org) and a PR_RECIPIENT_TYPE (MAPI_TO, MAPI_CC, MAPI_BCC).
table = message.GetRecipientTable(0) table.SetColumns([PR_DISPLAY_NAME, PR_EMAIL_ADDRESS], 0)
There are also some properties on the message that can be used to get a summary of the recipients. These are called PR_DISPLAY_TO, PR_DISPLAY_CC, PR_DISPLAY_BCC. They are simply PR_DISPLAY_NAME of all the recipients concatenated together. Since the properties are generated from the information in the recipient table, you cannot write these recipients.
If you wish to modify a row in the recipient table, you can do so with the ModifyRecipients() method:
message.ModifyRecipients(0, [ [ SPropValue(PR_DISPLAY_NAME, 'Joe'), SPropValue(PR_EMAIL_ADDRESS, 'joe@domain'), SPropValue(PR_RECIPIENT_TYPE, MAPI_TO) ] ])
The format of the passed recipients array is exactly the same as a set of rows returned by QueryRows(): an array of arrays of SPropValues.
This will replace the recipient table with the passed recipients. This allows you to add multiple recipients at once, or clear the recipient table (by passing 0 recipients). The PR_DISPLAY_* properties on the message will then automatically be updated.
Like properties, changes in the recipient table are not actually saved until the SaveChanges() method is called.
Tables normally show rows for all the messages in a folder. MAPI also provides the possibility to filter the messages according to a restriction. A restriction is a tree of expressions including AND, OR and other expressions. Each message that matches the expression is included in the table, and each message that does not match is discarded.
Since the restriction only applies to a view of the messages, the restriction can be switched easily from a full view to a restricted view and back within the same table instance:
table.Restrict(SRestriction(RES_PROPERTY, RELOP_EQ, PR_SUBJECT, SPropValue(PR_SUBJECT, 'this subject')))
This will restrict the view to messages with a PR_SUBJECT of this subject. If there are no matching messages, no rows will be available.
Until now we have only worked with the Inbox folder. MAPI supports a hierarchy of folders, including Inbox, Outbox, Sent Items, etc. The visible part of the tree (the part you see in the webaccess) is actually not the root of the folder structure, the part of the visible tree starts at a folder called the IPM_SUBTREE.
To get the entry id (unique identifier) of this folder, we can query a property of the store:
props = store.GetProps([PR_IPM_SUBTREE_ENTRYID], 0) entryid = props.Value ipmsubtree = store.OpenEntry(entryid)
The IPM_SUBTREE folder can now be used to get the folder hierarchy. This is accomplished by opening the hierarchy table of the folder. The hierarchy table is just like the contents table used earlier, except that the hierarchy table represents folders instead of messages.
table = ipmsubtree.GetHierarchyTable(0) table.SetColumns([PR_DISPLAY_NAME, PR_ENTRYID], 0) rows = table.QueryRows(10,0)
To get a view of all the folders, we need to open each folder, and for each folder loop through its children folders by examining the hierarchy table of each folder.
3.11. Default folders¶
Several folders like inbox, outbox, calendar, etc have a special status - they are default folders. MAPI clients will assign special behaviour to these folders. For example, the default folders cannot be renamed, deleted or moved. This limitation is completely client-enforced - MAPI itself doesn’t really care if you delete your inbox.
The default folders can be identified by several properties that contain the unique ID for those folders:
props = store.GetProps([PR_IPM_OUTBOX_ENTRYID, PR_IPM_TRASH_ENTRYID], 0) result = store.GetReceiveFolder('IPM', 0) inboxid = result inbox = store.OpenEntry(inboxid, None, 0) props = inbox.GetProps([PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_TASKS_ENTRYID, PR_IPM_CONTACTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID], 0)
As you can see, some properties for the default folders are set on the store, while others are set on the inbox object. The inbox object itself cannot be found by using a PR_IPM_* property, but only by using the GetReceiveFolder() function.
Removing, moving or even renaming default folders can cause your MAPI applications to malfunction. It can also cause your MAPI client to generate a new set of folders, effectively hiding your old folders from the user.