# IMAP email in Python

Receive and process email from any IMAP mailbox, including Microsoft 365 with OAuth2.

Python services receive email through IMAP connections defined in the Dashboard - messages can be fetched, marked as seen and deleted, and a connection can poll its mailbox on a schedule, invoking your service with each message it finds.

The same code works with classic IMAP servers and with Microsoft 365 connections that use OAuth2 - the connection holds the credentials and your Python code stays the same, with no knowledge of Microsoft 365 or OAuth2 required.

## Create a connection {#create-a-connection}

To create a connection, go to **Connections** > **Outgoing** > **E-mail** > **IMAP** in the Dashboard, click **Create a new IMAP connection** and fill in the form:

1. **Name:** My Connection
2. **User / e-mail:** the mailbox to read from
3. Click **Toggle options** in the Generic IMAP row and fill in **Host**, e.g. `imap.example.com`
4. Click **OK**

[Video: New IMAP connection](https://zatosource-production.b-cdn.net/docs/gfx/email/imap-create.webm?v=1791135952)

Dashboard menu: Connections > Outgoing > E-mail > IMAP

## Receive email {#receive-email}

The connection's `get` method yields each message that matches the connection's get criteria, e.g. `UNSEEN`:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):
        conn = self.email.imap.get('My Connection').conn

        for msg_id, msg in conn.get():

            # Access the message
            self.logger.info(msg.data)
```

## Mark messages as seen {#mark-messages-as-seen}

Call `.mark_seen()` on a message object and the server marks it as seen:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):
        conn = self.email.imap.get('My Connection').conn

        for msg_id, msg in conn.get():

            # Access the message
            self.logger.info(msg.data)

            # To mark the message seen
            msg.mark_seen()
```

## Delete messages {#delete-messages}

Call `.delete()` on a message object and the server marks it as deleted:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class MyService(Service):

    def handle(self):
        conn = self.email.imap.get('My Connection').conn

        for msg_id, msg in conn.get():

            # Access the message
            self.logger.info(msg.data)

            # To delete the message
            msg.delete()
```

## Read email on a schedule {#read-email-on-a-schedule}

Instead of polling a mailbox in your own code, an IMAP connection can do it for you. Expand the **Scheduler** section when you create or edit a connection in the Dashboard and fill in these fields:

- **Run every** - how often to poll the mailbox, e.g. every 5 minutes
- **Start date** - when to start polling
- **Service** - the service to invoke for each message received
- **Invoke with** - what the service receives on input, either **Message** or **Each attachment**

Saving the connection auto-creates a scheduler job for it. On each run, the job fetches all the messages matching the connection's get criteria, e.g. `UNSEEN`, and invokes your service with each of them, depending on the invoke-with mode.

After a message is processed, it is acknowledged automatically - if your service raises no exception, the message is marked as seen. If the service raises an exception, the message is left untouched, so it still matches the `UNSEEN` criteria and your service receives it anew on the next run. Messages are never lost when processing fails and there is no need to call `msg.mark_seen()` yourself.

### Invoke with messages {#invoke-with-messages}

In the **Message** mode, which is the default one, your service receives the message object on input, as yielded by `conn.get()`, so it can read the message's contents:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ProcessNewEmail(Service):

    def handle(self):

        # The IMAP message that we were invoked with
        msg = self.request.input

        # Access the message
        self.logger.info('Subject: %s', msg.data.subject)
        self.logger.info('From: %s', msg.data.sent_from)
        self.logger.info('Body: %s', msg.data.body['plain'])
```

### Invoke with attachments {#invoke-with-attachments}

In the **Each attachment** mode, the message's body is ignored and your service is invoked once for each attachment that the message contains. A message without attachments is acknowledged without invoking anything.

The input is an `IMAPAttachment` object with these attributes:

- `filename` - the attachment's file name
- `content_type` - its MIME type, e.g. `application/pdf`
- `size` - the size of the attachment's data, in bytes
- `data` - the attachment's contents, as bytes
- `content_id` - the attachment's Content-ID header or None if there was none
- `message` - the message object that the attachment was extracted from
- `msg_uid` - the UID of that message
- `subject` - the subject of that message
- `sent_from` - who sent that message

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ProcessNewAttachment(Service):

    def handle(self):

        # The IMAP attachment that we were invoked with
        attachment = self.request.input

        # Access the attachment
        self.logger.info('File name: %s', attachment.filename)
        self.logger.info('Content type: %s', attachment.content_type)
        self.logger.info('Size: %s', attachment.size)
        self.logger.info('From message: %s', attachment.subject)

        # The contents is available as bytes
        data = attachment.data
```

If any of the per-attachment invocations raises an exception, the whole message is left untouched and all of its attachments are received anew on the next run.

#### Attachments that are archives {#attachments-that-are-archives}

An attachment that is a zip archive is read with `attachment.documents()`, which returns each file inside it as a document with its name, MIME type and bytes, and an attachment that is not an archive comes back as the one document it is. A zip that is an [IHE XDM package](https://zato.io/docs/dev/healthcare/xdm/index.html) of clinical documents returns the documents its metadata names, each with its title, patient ID and document codes. In the **Message** mode, `msg.documents()` returns the documents of all of the message's attachments in order.

## See also {#see-also}

- [SMTP](https://zato.io/docs/dev/examples/smtp.html) - Send plain-text and HTML email with attachments
- [Scheduler](https://zato.io/docs/dev/examples/scheduler.html) - Run services on an interval, beyond mailbox polling
- [File transfer](https://zato.io/docs/dev/file-transfer/index.html) - Receive files over SFTP and SMB instead of email

## Learn more {#learn-more}

- [Development documentation](https://zato.io/docs/dev/) - The complete reference for writing services
- [Requests and responses](https://zato.io/docs/dev/request-response/) - What a service receives, what it returns and how both are structured
- [Integration examples](https://zato.io/docs/dev/examples/) - Ready-made code for the systems you are likely to connect to
- [IDE and debugging](https://zato.io/docs/dev/ide/) - Write services in the Dashboard or in your own editor
- [Data models](https://zato.io/docs/dev/model/) - Declare inputs and outputs and have them validated for you
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - The full platform tutorial, from installation to production patterns
