Skip to content

IMAP Push Notifications

Ralf Becker edited this page Jan 23, 2022 · 14 revisions

From EGroupware 20.1 on supported IMAP servers can be configured to send push notifications to EGroupware's push server.

Currently only supported is Dovecot on premise and EGroupware GmbH's mail service for the hosting (mail.egroupware.org).

If you use our hosting, nothing special need to be done, everything is already preconfigured for you.

General steps for all mail-servers

  • First you need to find out your automatic generate bearer-token for push:
docker exec -it egroupware-push cat /var/www/config.inc.php
<?php
$bearer_token = '<bearer-token>';
  • If the bearer-token has URL special characters + or / in it, you need to change them first, eg. replace them with an X:
cd /etc/egroupware-docker
docker run --rm -it -vegroupware-docker_push-config:/mnt busybox vi /mnt/config.inc.php
docker-compose restart
  • Add IMAP server name (or IP-addresse) used in EGroupware in Mail site configuration: Admin > Applications > Mail > Site configuration

Dovecot 2.2+

Dovecot 2.2 only supports notifications about new arriving mails in users INBOX AND only http URLs!

Create the following file /etc/dovecot/conf.d/99-egroupware-push.conf

# Store METADATA information in a file dovecot-metadata in user's home
mail_attribute_dict = file:%h/dovecot-metadata

# enable metadata
protocol imap {
  imap_metadata = yes
}

# add notify AND push_notification plugins for LMTP (or LDA if you use that)
protocol lmtp {
  mail_plugins = $mail_plugins notify push_notification
}

# URL to call for new arriving mails in the INBOX
plugin {
  push_notification_driver = ox:url=http://Bearer:<push-token>@<egroupware-domain>/egroupware/push user_from_metadata
}

Then restart Dovecot: systemctl restart dovecot

Dovecot documentation about push and required IMAP metadata

Dovecot 2.3.7+

Dovecot 2.3 allows to get more events (flag changes, delete or Sieve moved mails) via a custom LUA script and allows https URLs. 2.3.7+ is required to read metadata information / the push server tokens EGroupware places on the mailbox (metadata_get() method).

Install dovecot-lua plus required Lua modules:

apt install dovecot-lua lua-socket lua-json
curl https://raw.githubusercontent.com/EGroupware/swoolepush/master/doc/dovecot-push.lua > /etc/dovecot/dovecot-push.lua

(For CentOS/RHEL use yum instead of apt and you need to install epel-release package/repo first. Debian/Ubuntu has no dovecot-lua, it is include in dovecot-core already.)

Create the following file /etc/dovecot/conf.d/14-egroupware-push.conf (must be before 15-lda.conf and 20-lmtp.conf!)

# Store METADATA information in a file dovecot-metadata in user's home
mail_attribute_dict = file:%h/dovecot-metadata

# enable metadata
protocol imap {
  imap_metadata = yes
}

# add necessary plugins for Lua push notifications
mail_plugins = $mail_plugins mail_lua notify push_notification push_notification_lua

# Lua notification script and URL of EGroupware push server
plugin {
  push_notification_driver = lua:file=/etc/dovecot/dovecot-push.lua
  push_lua_url = https://Bearer:<push-token>@<egroupware-domain>/egroupware/push
}

The above commands installed the following Lua script to /etc/dovecot/dovecot-push.lua

-- To use
--
-- plugin {
--  push_notification_driver = lua:file=/etc/dovecot/dovecot-push.lua
--  push_lua_url = https://Bearer:<push-token>@<egroupware-domain>/egroupware/push
-- }
--
-- server is sent a PUT message with JSON body like push_notification_driver = ox:url=<push_lua_url> user_from_metadata
-- plus additionally the events MessageAppend, MessageExpunge, FlagsSet and FlagsClear
-- MessageTrash and MessageRead are ignored, so are empty or NonJunk FlagSet/Clear from TB
--
-- Needs lua-socket and lua-json packages plus dovecot-lua!
--

local http = require "socket.http"
local ltn12 = require "ltn12"
local json = require "json"

function table_get(t, k, d)
  return t[k] or d
end

function dovecot_lua_notify_begin_txn(user)
    local meta = user:metadata_get("/private/vendor/vendor.dovecot/http-notify")
    if (meta == nil or meta:sub(1,5) ~= "user=")
    then
        meta = nil;
    else
        meta = meta:sub(6)
    end
    return {user=user, event=dovecot.event(), ep=user:plugin_getenv("push_lua_url"), messages={}, meta=meta}
end

function dovecot_lua_notify_event_message_new(ctx, event)
    -- check if there is a push token registered
    if (ctx.meta == nil) then
        return
    end
    -- get mailbox status
    local mbox = ctx.user:mailbox(event.mailbox)
    mbox:sync()
    local status = mbox:status(dovecot.storage.STATUS_RECENT, dovecot.storage.STATUS_UNSEEN, dovecot.storage.STATUS_MESSAGES)
    mbox:free()
    table.insert(ctx.messages, {
        user = ctx.meta,
        ["imap-uidvalidity"] = event.uid_validity,
        ["imap-uid"] = event.uid,
        folder = event.mailbox,
        event = event.name,
        from = event.from,
        subject = event.subject,
        snippet = event.snippet,
        unseen = status.unseen,
        messages = status.messages
    })
end

function dovecot_lua_notify_event_message_append(ctx, event)
  dovecot_lua_notify_event_message_new(ctx, event)
end

-- ignored, as FlagSet flags=[\Seen] is sent anyway too
-- function dovecot_lua_notify_event_message_read(ctx, event)
--    dovecot_lua_notify_event_message_expunge(ctx, event)
-- end

-- ignored, as most MUA nowadays expunge immediatly
-- function dovecot_lua_notify_event_message_trash(ctx, event)
--    dovecot_lua_notify_event_message_expunge(ctx, event)
-- end

function dovecot_lua_notify_event_message_expunge(ctx, event)
    -- check if there is a push token registered
    if (ctx.meta == nil) then
        return
    end
    -- get mailbox status
    local mbox = ctx.user:mailbox(event.mailbox)
    mbox:sync()
    local status = mbox:status(dovecot.storage.STATUS_RECENT, dovecot.storage.STATUS_UNSEEN, dovecot.storage.STATUS_MESSAGES)
    mbox:free()
    -- agregate multiple Expunge (or Trash or Read)
    if (#ctx.messages == 1 and ctx.messages[1].user == ctx.meta and ctx.messages[1].folder == event.mailbox and
        ctx.messages[1]["imap-uidvalidity"] == event.uid_validity and ctx.messages[1].event == event.name)
    then
        if (type(ctx.messages[1]["imap-uid"]) ~= 'table') then
            ctx.messages[1]["imap-uid"] = {ctx.messages[1]["imap-uid"]}
        end
        table.insert(ctx.messages[1]["imap-uid"], event.uid)
        ctx.messages[1].unseen = status.unseen
        ctx.messages[1].messages = status.messages
        return;
    end
    table.insert(ctx.messages, {
        user = ctx.meta,
        ["imap-uidvalidity"] = event.uid_validity,
        ["imap-uid"] = event.uid,
        folder = event.mailbox,
        event = event.name,
        unseen = status.unseen,
        messages = status.messages
    })
end

function dovecot_lua_notify_event_flags_set(ctx, event)
    -- check if there is a push token registered
    if (ctx.meta == nil or
        (#event.flags == 0 and #event.keywords == 0) or -- ignore TB sends it empty
        (#event.keywords == 1 and event.keywords[1] == "NonJunk")) -- ignore TB NonJunk
    then
        return
    end
    local status = nil;
    if (#event.flags == 1 and event.flags[1] == "\\Seen")
    then
        -- get mailbox status
        local mbox = ctx.user:mailbox(event.mailbox)
        mbox:sync()
        status = mbox:status(dovecot.storage.STATUS_RECENT, dovecot.storage.STATUS_UNSEEN, dovecot.storage.STATUS_MESSAGES)
        mbox:free()
    end
    -- agregate multiple FlagSet
    if (#ctx.messages == 1 and ctx.messages[1].user == ctx.meta and ctx.messages[1].folder == event.mailbox and
        ctx.messages[1]["imap-uidvalidity"] == event.uid_validity and ctx.messages[1].event == event.name and
        arrayEqual(ctx.messages[1].flags, event.flags) and arrayEqual(ctx.messages[1].keywords, event.keywords))
    then
        if (type(ctx.messages[1]["imap-uid"]) ~= 'table') then
            ctx.messages[1]["imap-uid"] = {ctx.messages[1]["imap-uid"]}
        end
        table.insert(ctx.messages[1]["imap-uid"], event.uid)
        if (status ~= nil)
        then
            ctx.messages[1].unseen = status.unseen
        end
        return;
    end
    local msg = {
        user = ctx.meta,
        ["imap-uidvalidity"] = event.uid_validity,
        ["imap-uid"] = event.uid,
        folder = event.mailbox,
        event = event.name,
        flags = event.flags,
        keywords = event.keywords
    }
    if (status ~= nil)
    then
        msg.unseen = status.unseen
    end
    if (event.name == "FlagsClear")
    then
        msg.flags_old = event.flags_old
        msg.keywords_old = event.keywords_old
    end
    table.insert(ctx.messages, msg)
end

function arrayEqual(t1, t2)
    if (#t1 ~= #t2)
    then
        return false
    end
    if (#t1 == 1)
    then
        return  t1[1] == t2[1]
    end
    return json.encode(t1) == json.encode(t2)
end

function dovecot_lua_notify_event_flags_clear(ctx, event)
    dovecot_lua_notify_event_flags_set(ctx, event)
end

function dovecot_lua_notify_end_txn(ctx)
    -- report all states
    for i,msg in ipairs(ctx.messages) do
        local e = dovecot.event(ctx.event)
        e:set_name("lua_notify_mail_finished")
        reqbody = json.encode(msg)
        e:log_debug(ctx.ep .. " - sending " .. reqbody)
        res, code = http.request({
            method = "PUT",
            url = ctx.ep,
            source = ltn12.source.string(reqbody),
            headers={
                ["content-type"] = "application/json; charset=utf-8",
                ["content-length"] = tostring(#reqbody)
            }
        })
        e:add_int("result_code", code)
        e:log_info("Mail notify status " .. tostring(code))
    end
end

Then restart Dovecot: systemctl restart dovecot

Cyrus

Cyrus allows to configure a script as external notification for it's notifyd. Let us know, if you're interested that we create such a script.

Clone this wiki locally