Incoming email
Receiving mail without the hosted service: a raw message over HTTP, an IMAP mailbox, or Postmark.
Alba Ticket receives mail as well as sending it: a reply to a notification becomes a comment, a message to a helpdesk form's address raises a request, and a member can log mail on a contact. The user guide's Email section says what administrators see; this page is how the mail gets in.
Everything is set under Administration, Email at run time, not in the environment: choose a provider, give the address mail arrives at, enter what the provider needs and switch Receive mail on. Three providers cover nearly every setup. Whichever you choose, send a message to the address afterwards and watch the page's Last message received change.
Which provider
| You have | Choose |
|---|---|
| A mail server of your own (Postfix, Exim) or a relay that can forward a message over HTTP (Cloudflare Email Routing, Mailgun routes, SendGrid inbound parse) | Raw message over HTTP |
| A mailbox at any provider (Google Workspace, Fastmail, your own IMAP server) | IMAP mailbox |
| A Postmark account | Postmark (see Postmark) |
A raw message over HTTP
The whole message is posted as message/rfc822 to /webhooks/mail/raw with a bearer secret: Generate a new secret on the Email page makes one, shows it once and keeps only its hash. Put the envelope recipient in an X-Original-To request header when your server knows it, so a message that reached the address through a list or a forward is routed by the address it was sent to. A request without the right secret is refused with 401 before the message is read, and a body over 64 MB is refused with 413.
Postfix
Give Postfix a transport that pipes the message to curl. In /etc/postfix/master.cf:
alba unix - n n - - pipe
flags=Rq user=nobody argv=/usr/local/bin/alba-inbound ${original_recipient}
In /etc/postfix/transport (then postmap /etc/postfix/transport), send the address, or the whole subdomain, to it:
support@example.com alba:
.in.example.com alba:
And the script, which reads the message from standard input:
#!/bin/sh
# /usr/local/bin/alba-inbound <recipient>
exec curl --silent --show-error --fail \
--header "Authorization: Bearer $(cat /etc/alba-inbound.secret)" \
--header "Content-Type: message/rfc822" \
--header "X-Original-To: $1" \
--data-binary @- \
https://tickets.example.com/webhooks/mail/raw
curl exits non-zero when the server answers anything but a 2xx, and Postfix then keeps the message in its queue and tries again, so a restart of Alba Ticket loses nothing. Exim's pipe transport works the same way.
Cloudflare Email Routing
An Email Worker receives the message and posts it on:
export default {
async email(message, env) {
const raw = await new Response(message.raw).arrayBuffer();
const response = await fetch("https://tickets.example.com/webhooks/mail/raw", {
method: "POST",
headers: {
"Authorization": `Bearer ${env.ALBA_INBOUND_SECRET}`,
"Content-Type": "message/rfc822",
"X-Original-To": message.to,
},
body: raw,
});
if (!response.ok) message.setReject(`Alba Ticket answered ${response.status}`);
},
};
Bind the secret as the Worker's ALBA_INBOUND_SECRET variable, route the address (or a catch-all for the subdomain) to the Worker, and let Cloudflare's DNS carry the MX records it asks for.
What the server vouches for
Nobody checks the sender on the raw path unless your receiving server does. When it adds an Authentication-Results header (Postfix with OpenDMARC or rspamd, Cloudflare Email Routing, most relays do), tick Trust the receiving server's Authentication-Results header and Alba Ticket reads the SPF, DKIM and DMARC results from it: a reply failing DMARC is refused and a request failing it is held for review. Without that, every check is recorded as not done, and a request from a sender who is neither a user nor a contact is held for an administrator to let in, since nothing says who sent it.
An IMAP mailbox
Alba Ticket reads a mailbox you own once a minute: the unseen messages of the folder are fetched, handed in, and moved to a Processed folder (made if missing), or marked seen where the server cannot move. Nothing is ever deleted. Give the host, port (993 with TLS, 143 without), user name, password and, if you like, the folders. The page shows when the mailbox was last read and the last error in words; a mailbox that has failed for a day is reported to administrators once as a notification.
The client speaks passwords only, not OAuth, so a provider that has turned password sign-in off cannot be read directly; forward the mailbox to one that allows it, or use the raw path.
- Google Workspace / Gmail: turn IMAP on in the account's settings and make an app password (two-step verification must be on); the host is
imap.gmail.com, port 993. Gmail has no MOVE restriction; the processed folder becomes a label. - Microsoft 365: basic authentication for IMAP is switched off for most organisations and cannot be turned back on, so read a Microsoft 365 mailbox through a forwarding rule to another mailbox, or through the raw path from a relay.
- Fastmail: make an app password with IMAP access; the host is
imap.fastmail.com, port 993. - Your own Dovecot: any user with IMAP access;
MOVEis supported from Dovecot 2.2.
Messages arrive at the mailbox's own address, so give that address as the inbound address; what follows a + in it routes the message as everywhere else, if your server delivers sub-addresses (Dovecot and Gmail do).
Postmark
With a Postmark account of your own, Postmark receives the mail and posts each message to the installation.
- In Postmark, open (or create) a server and its inbound message stream. The stream has an inbound address of its own at
inbound.postmarkapp.com; to receive at your own domain instead, add the domain under the stream's settings and point its MX record atinbound.postmarkapp.com. - Under Administration, Email, choose Postmark, give the address mail arrives at, and choose a webhook user name and password: any pair you make up. Switch Receive mail on and save.
- In the stream's settings, set the inbound webhook to the address below, with that user name and password in it, and tick Include raw email content in JSON payload, so that the message kept is the one that was received:
https://<user>:<password>@your.host/webhooks/mail/postmark
Postmark does not sign what it posts, so the user name and password are what proves a delivery is Postmark's: a wrong pair is answered 401 and nothing is kept. Use a long random password, and HTTPS. Postmark checks SPF, DKIM and DMARC and marks spam before it posts, and Alba Ticket reads its verdicts from the headers it adds: a reply failing DMARC is refused and a request failing it is held for review.
Postmark keeps a copy of each inbound message for as long as its retention setting says (45 days unless you change it). That copy is Postmark's, under your agreement with Postmark; set the retention to what your own privacy notice says.
DNS
For a subdomain of your own that receives mail (in.example.com), an MX record pointing at the server that receives it: your Postfix, Cloudflare's inbound servers, or Postmark's. Nothing else is needed for receiving; SPF, DKIM and DMARC records are about the mail you send, which Configuration covers. A reply address Alba Ticket gives out is <inbound address local part>+r-<token>@<domain>, so the server must deliver sub-addresses (plus addressing) to the one mailbox or pipe, which Postfix does with recipient_delimiter = +.