Skip to content

iOS Mail Push Support

Introduction

iOS Mail currently does not support the IMAP idle extension. Therefore users can only either check manually or configure intervals for fetching mails in their mail account preferences when using the default configuration.

To support mail push Dovecot needs to advertise the XAPPLEPUSHSERVICE IMAP extension as well as sending the actual push notifications to the Apple Push Notification service (APNs) which will forward them to the device.

This can be done with two components:

  • A Dovecot plugin (dovecot-xaps-plugin) which is triggered whenever a mail is created or moved from/to a mail folder.
  • A daemon service (dovecot-xaps-daemon) that manages both the device registrations as well as sending notifications to the APNs.

Prerequisites

  • An Apple developer account to create the required Apple Push Notification service certificate.
  • Knowledge creating Docker images, using the command-line, and creating shell scripts.

Limitations

  • You need to maintain a custom docker-mailserver image.
  • Push support is limited to the INBOX folder. Changes to other folders will not be pushed to the device regardless of the configuration settings.
  • You currently cannot use the same account UUID on multiple devices. This means that if you use the same backup on multiple devices (e.g. old phone / new phone) only one of them will get the notification. Use different backups or recreate the mail account.

Privacy concerns

  • The service does not send any part of the actual message to Apple.
  • The information sent contains the device UUID to notify and the (on-device) account UUID which was generated by the iOS mail application when creating the account.
  • Upon receiving the notification, the iOS mail application will connect to the IMAP server given by the provided account UUID and fetch the mail to notify the user.
  • Apple therefore does not know the mail address for which the mail was received, only that a specific account on a specific device should be notified that a new mail or that a mail was moved to the INBOX folder.

Installation

Both components will be built using Docker and included into a custom docker-mailserver image. Afterwards the required configuration is added to docker-data/dms/config. The registration data is stored in /var/mail-state/lib-xapsd.

  1. Create a Dockerfile to build a docker-mailserver image that includes the dovecot-xaps-plugin as well as the dovecot-xaps-daemon. This is required to ensure that the Dovecot plugin is built against the same Dovecot version. The :edge tag is used here, but you might want to use a released version instead.

    FROM mailserver/docker-mailserver:edge AS dovecot-plugin-xaps
    WORKDIR /tmp/dovecot-xaps-plugin
    RUN <<EOF
        apt-get update
        apt-get -y --no-install-recommends install git cmake make build-essential dovecot-dev
        git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-plugin.git .
        mkdir build && cd build
        cmake .. -DCMAKE_BUILD_TYPE=Release
        make install
    EOF
    
    # Use an older Go version as Go >= 1.20 causes this issue: https://github.com/freswa/dovecot-xaps-daemon/issues/24#issuecomment-1483876081
    # Note that the underlying issue are non-standard-compliant Apple http servers which might get fixed at some point
    FROM golang:1.19-alpine AS dovecot-xaps-daemon
    ENV GOPROXY=https://proxy.golang.org,direct
    ENV CGO_ENABLED=0
    WORKDIR /go/dovecot-xaps-daemon
    RUN <<EOF
        apk add --no-cache --virtual build-dependencies git
        git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-daemon .
        go build ./cmd/xapsd
    EOF
    
    FROM mailserver/docker-mailserver:edge
    COPY --from=dovecot-plugin-xaps /usr/lib/dovecot/modules/*_xaps_* /usr/lib/dovecot/modules/
    COPY --from=dovecot-xaps-daemon /go/dovecot-xaps-daemon/xapsd /usr/bin/xapsd
    
    # create a non-root user for the daemon process as well as configuration and run state directories
    RUN <<EOF
        adduser --quiet --system --group --disabled-password --home /var/mail-state/lib-xapsd --no-create-home xapsd
        mkdir -p /var/run/xapsd /etc/xapsd
    EOF
    
  2. Build the new image:

    docker build -t yourname/docker-mailserver .
    

  3. Modify your compose.yaml to use the newly created image:

        services:
            mailserver:
              image: yourname/docker-mailserver:latest
    

  4. Recreate the container:

    docker compose down
    docker compose up -d
    

  5. Create a hash of your Apple developer account password using the provided xapsd -pass command:

    docker exec -it mailserver xapsd -pass
    

  6. Add configuration for both components:

    • Create a folder named xaps in docker-data/dms/config.

    • Create a file named xapsd.yaml in docker-data/dms/config/xaps.

      • Replace appleId and appleIdHashedPassword with your actual credentials. For reference see also here.
      • The service will use the provided username/hash combination to automatically request a new certificate from Apple as well as renewing an older certificate if needed.
      xapsd.yaml
      # set the loglevel to either
      # trace, debug, error, fatal, info, panic or warn
      # Default: info
      loglevel: info
      
      # xapsd creates a json file to store the registration persistent on disk.
      # This sets the location of the file.
      databaseFile: /var/mail-state/lib-xapsd/database.json
      
      # xapsd listens on a socket for http/https requests from the dovecot plugin.
      # This sets the address and port number of the listen socket.
      listenAddr: '127.0.0.1'
      port: 11619
      
      # xapsd is able to listen on a HTTPS Socket to allow HTTP/2 to be used
      # SSL is enabled implicitly when certfile and keyfile exist
      # !!! only use HTTPS for connection pooling with a proxy e.g. nginx or HaProxy
      # !!! direct usage with the plugin is discouraged and unsupported
      tlsCertfile:
      tlsKeyfile:
      tlsListenAddr:
      tlsPort: 11620
      
      # Notifications that are not initiated by new messages are not sent immediately for two reasons:
      # 1. When you move/copy/delete messages you most likely move/copy/delete more messages within a short period of time.
      # 2. You don't need your mailboxes to synchronize immediately since they are automatically synchronized when opening
      #    the app
      # If a new message comes and the move/copy/delete notification is still on hold it will be sent with the notification
      # for the new message.
      # This sets the interval to check for delayed messages.
      checkInterval: 20
      
      # Set the time how long notifications for not-new messages should be delayed until they are sent.
      # Whenever checkInterval runs, it checks if "delay" <= "waiting time" and sends the notification if the expression is
      # true.
      delay: 30
      
      # To retrieve certificates from Apple, we need to login with a valid Apple ID
      # The accounts email must be given in cleartext, but the password has to
      # be hashed before sending it. To not leak working credentials on running servers,
      # we do not accept the cleartext password here.
      appleId: foo@example.com
      
      # use `xaps -pass` to calculate the hash of the apple id password
      appleIdHashedPassword: bar
      
    • Create a file named 95-xaps.conf in docker-data/dms/config/xaps. For reference see also here.

      95-xaps.conf
      protocol imap {
        mail_plugins = $mail_plugins notify push_notification xaps_push_notification xaps_imap
      }
      
      protocol lda {
        mail_plugins = $mail_plugins notify push_notification xaps_push_notification
      }
      
      protocol lmtp {
        mail_plugins = $mail_plugins notify push_notification xaps_push_notification
      }
      
      plugin {
          # xaps_config contains xaps specific configuration parameters
          # url:              protocol, hostname and port under which xapsd listens
          # user_lookup: Use if you want to determine the username used for PNs from environment variables provided by
          #                   login mechanism. Value is variable name to look up.
          # max_retries:      maximum num of retries the http client connects to the xaps daemon
          # timeout_msecs     http timeout of the http connection
          xaps_config = url=http://127.0.0.1:11619 user_lookup=theattribute max_retries=6 timeout_msecs=5000
          push_notification_driver = xaps
      }
      

    • Create a supervisord file named xapsd.conf in docker-data/dms/config/xaps with the following content:

      xapsd.conf
      [program:xapsd]
      startsecs=0
      autostart=false
      autorestart=true
      stdout_logfile=/var/log/supervisor/%(program_name)s.log
      stderr_logfile=/var/log/supervisor/%(program_name)s.log
      user=xapsd
      command=/usr/bin/xapsd
      pidfile=/var/run/xapsd/xapsd.pid
      

    • Create or update your user-patches.sh in docker-data/dms/config to move the files to their final location as well as starting the daemon service:

      user-patches.sh
      #!/bin/bash
      
      # Copy the configs to internal locations:
      cp /tmp/docker-mailserver/xaps/95-xaps.conf /etc/dovecot/conf.d/95-xaps.conf
      cp /tmp/docker-mailserver/xaps/xapsd.yaml /etc/xapsd/xapsd.yaml
      cp /tmp/docker-mailserver/xaps/xapsd.conf /etc/supervisor/conf.d/xapsd.conf
      
      # Setup data persistence and ensure ownership is always for xapsd:
      mkdir -p /var/mail-state/lib-xapsd
      chown -R xapsd:xapsd /var/mail-state/lib-xapsd
      
      # Start the xaps daemon:
      supervisorctl update
      supervisorctl start xapsd
      

  7. Recreate the container again to apply the new configuration:

    docker compose down
    docker compose up -d
    

  8. Recreate your mail account on your iOS device and check the logs in /var/log/supervisor/dovecot.log and /var/log/supervisor/xapsd.log for any errors.

Other configuration options

Both device registration and notifications send a username to the daemon to lookup the device. While the registration and other IMAP operations in Dovecot will send the Dovecot username, LMTP will send the provided authentication username.

The format of that username is specified by the auth_username_format Dovecot setting. If you are not using mail addresses as Dovecot usernames - e.g. when using LDAP - you can either change the auth_username_format or add the mail address as property to the user account and use the lookup feature (see below).

user-patches.sh
sed -i -r "s|^#?(auth_username_format =).*|\1 %Ln|" /etc/dovecot/conf.d/10-auth.conf

You can also use notifications for Dovecot alias mailboxes. Depending on your server configuration, this might require to add the original Dovecot username as Dovecot attribute to the login user as well as changing the user_lookup=theattribute in 95-xaps.conf to perform the lookup of that attribute.