receiving email / inbound smtp

Hi! I’m interested in processing incoming email on fly for a project of mine. I’m picturing the fly app handling smtp + email parsing, then sending messages to a separate http service for handling. This seems like a convenient way to run the smtp part, and lets me keep my http server behind cloudflare (compared to also handling smtp on that server and pointing an unproxyed mx record at it). I gave this a shot with the experimental config mentioned in Custom TCP ports but couldn’t seem to get anything through on port 25.

Obviously, smtp and 25/465/587 are kind of fraught, so I’m not surprised this doesn’t work. Going by Custom inbound TCP port & long living connections is it possible it would work in the future?

If that’s the case, I’d also be interested to hear your thoughts on how tls would work. I imagine I wouldn’t be able to use the tls handler because of starttls, so either I’d need to provision certs myself or somehow access the ones that fly provisions.

2 Likes

I think we should open the SMTP ports for inbound email processing. We’re about halfway done allowing all ports, but we can look at doing these early. This sounds like a cool app to me!

TLS is interesting, I honestly don’t know much about TLS for SMTP. Assuming mail servers do SNI properly, it may just work with our TLS handler. If that doesn’t work, you will need your own certs. We’ve had requests for API access to the certs we manage but haven’t tackled that yet.

3 Likes

I’m also working on something that involves receiving inbound SMTP and would love to be able to accept connections on port 25.

We’re going to try to get this in for you in the next few days. These sound like really fun apps.

2 Likes

That would be awesome. Looking forward to it.

Happy weds! You should be able to add port 25 to your app configurations and accept email now. Give it a test and let us know how it goes.

3 Likes

Just gave it a try, it lets me deploy the application on port 25, but I’m unable to connect to it from the outside.

From my test machine:
telnet {ip} 25 results in telnet: Unable to connect to remote host: Connection refused

Whereas:
telnet gmail-smtp-in.l.google.com 25 actually connects, so I at least know the issue isn’t outbound filtering.

I had the app previously successfully accepting mail by deploying it on port 10000 and proxying port 25 traffic to it from another server, and it’s passing the tcp healthchecks. Could the inbound port be blocked somewhere on your end?

Can you try it again please? We just noticed a small configuration error but I think it should work for you now!

It works. Thanks for opening this up, we’re really excited about using Fly!

3 Likes

Port 25 is looking good for me too! Thanks.

I looked into tls briefly. My understanding is that a typical expectation for mail servers is:

  • port 25: try to upgrade via starttls; server decides whether non-upgraded sessions are allowed
  • port 465: smtp over tls
  • port 587: like port 25, but require tls upgrading

The port 465 standard does seem like it could work if the port was available: I set up a tls handler on another port and got mail through with swaks --tls-sni myapp.fly.dev --tlsc .... That flag was only recently added to swaks, though, so I have no idea how common it is for mail clients to actually do sni.

Ports 25 and 587 seem like they would work if I managed certs myself. Conceptually it seems simple to have something periodically write certs to a fly secret, but I’m fuzzy on the details. Doing an acme http challenge from my app on fly seems hairy since I’d need to serve smtp and http from one image, right? Or maybe I could run something externally that uses a dns challenge? Access to the raw TLS certs via the Fly API would also solve this, I think.

1 Like

It seems like SNI might work with port 465 with our built in TLS handler. Postfix sends SNI too, it might be well supported.

You don’t have to run an HTTP server on the same app, you can do DNS based verification.

I’d been meaning to set up cert-manager anyway, so I’ve now got that running outside of fly with dns challenges. I’m still working on the best way to deploy the certs.

I was planning to put the key + cert into secrets so that my external cron could run flyctl secrets set instead of a build/deploy. But, the cert (in pem format) turns out to be too big for that: it’s ~5kb and secrets are limited to 4kb. For now I’ve set it via an env var in flyctl deploy.

I think could avoid a build by finding the current image via fly’s registry and passing it to flyctl deploy --image, but that seems like a hassle. My best idea right now is to just chop up the cert into 4kb chunks and concat them at startup. Anyone got a better idea?

I wonder why we restricted secrets to that size. We’ll see if we can up that limit tomorrow.

1 Like

Just investigating this, and noticed that the client IP (eg 93.187.216.173) that connects on port 25 is the edge proxy IP (I assume anyway, since it’s always the same regardless of which client I actually connect from).

Is there an option to setup a SMTP handler, like the HTTP ones, that support the SMTP proxy functionality, so that we can get the real client IP ?

Just a stab, but: getting the original source is the use case for the proxy_proto handler:

https://fly.io/docs/reference/services/#proxy-protocol

Your SMTP software would need to grok the HAProxy proxy protocol to make that work.

A good stab! works like a charm with postfix/postscreen Postfix Configuration Parameters

I’m glad this works for you but sad that this collapses a giant ambitious nightmare network feature I just proposed to Kurt.

2 Likes

Has the limit been raised?

To close the loop here: I finally got back to this project and everything is working beautifully! The secrets limit has been raised, and now that all ports are allowed I can run on 25, 465, and 587.

In case it’s helpful for others working on smtp, here’s the relevant bits of my fly.toml:

[[services]]
  internal_port = 25
  protocol = "tcp"

  [[services.ports]]
    port = 25

  [[services.ports]]
    handlers = ["tls"]
    port = 465

  [[services.ports]]
    port = 587

Thanks for all the improvements along the way! I’m really excited to cut traffic over from my old server.

4 Likes

I’d be interested in seeing your postfix config & dockerfile if you are able to share. I’m struggling right now with starttls specifically, and am wondering if you have solved that.

Thanks for sharing :slight_smile: