JustEmails
PricingSign inStart free trialStart free
Tutorials··12 min read

Gitea SMTP Mailer Setup (and Forgejo) in 6 Steps

The Gitea SMTP mailer block in app.ini, Forgejo included — PROTOCOL vs MAILER_TYPE, FROM vs ENVELOPE_FROM, and how to actually test it.

By JustEmails Platform Team
Contents
  1. What We're Building
  2. Prerequisites
  3. Step 1: Create a Dedicated Mailbox for the Forge
  4. Step 2: PROTOCOL, Not MAILER_TYPE
  5. Step 3: Write the Gitea SMTP `[mailer]` Block
  6. Step 4: FROM vs ENVELOPE_FROM
  7. Step 5: Turn On the Mail That Actually Matters
  8. Step 6: Test It — and What `doctor` Won't Tell You
  9. Common Gitea SMTP Errors and Fixes
  10. Next Steps
  11. Frequently Asked Questions
  12. What's the difference between PROTOCOL and MAILER_TYPE in Gitea?
  13. Why does Gitea say the test email sent but nothing arrives?
  14. Do I need ENVELOPE_FROM in the Gitea mailer config?
  15. Can gitea doctor test my SMTP settings?
  16. Try JustEmails

Friday, 4:50 PM. A contractor pings me saying he can't get into our Forgejo instance and the password-reset email never showed up. Not in spam. Not anywhere. So I went looking at the Gitea SMTP settings, which is where this gets embarrassing.

It had never left the box. The [mailer] section in app.ini was still commented out from the day I spun the container up, six weeks earlier, and nobody had needed a reset until then — so nothing had ever looked broken. Registration confirmations: dead. Every pull request notification since day one: dropped on the floor, silently, because Gitea's mailer just no-ops when it's disabled.

That's the thing about self-hosted forges. They fail quietly. Your CI screams when it breaks, but mail that never sends doesn't page anyone.

Here's the whole Gitea SMTP setup, for both forks, including the two config keys that trip up almost everyone.

What We're Building

A working [mailer] block in app.ini that pushes mail through authenticated SMTP on JustEmails, signed with DKIM, landing in inboxes instead of junk. Specifically:

  • Registration confirmation emails for new accounts
  • Password reset links that actually arrive
  • Issue and pull request notifications — which, fair warning, need a second setting flipped in a completely different section of the file (Step 5)
  • A From address on your own domain, not gitea@localhost

Same block works on Gitea and Forgejo. I'll flag the two spots where they diverge.

Prerequisites

  • Gitea 1.21+ or Forgejo 1.21 / v7+. Version matters here more than usual — the mailer keys were renamed and half the tutorials online still use the dead ones.
  • Shell access to wherever app.ini lives. Bare metal: /etc/gitea/app.ini. Official Docker images for both forks: /data/gitea/conf/app.ini (yes, Forgejo kept the gitea path for drop-in compatibility).
  • A JustEmails account with your domain added and DNS green.
  • The ability to restart the service. Gitea doesn't hot-reload mailer config — edits do nothing until it comes back up.

One quick check before you start: nc -zv smtp.justemails.app 587 from the forge host. If that hangs, your provider is blocking outbound 587 and no amount of config will save you. Which is its own special kind of maddening, because nobody puts that on the pricing page — you find out at 11 PM, staring at dead silence on the wire with a config you know is correct.

Step 1: Create a Dedicated Mailbox for the Forge

In your JustEmails dashboard, make a mailbox like git@yourdomain.com. Not your personal address, not a shared one.

Why bother? The credential goes in a plaintext config file on a server other people can read. When you rotate it — and you will — the blast radius should be one machine, not your inbox. The $49/year plan includes unlimited mailboxes, so a dedicated one costs nothing.

I didn't do this the first time. Used my own address, forgot about it, and found those credentials eight months later in a config file on a box two contractors had root on. Nothing came of it. Easily could have.

Resist the urge to name it noreply@. Gitea sends password resets and notification mail that people genuinely reply to, and dumping those replies into a black hole is a bad look on an internal tool — we've argued this at length.

Grab these four values:

  • Host: smtp.justemails.app
  • Port: 587
  • Username: the full address, git@yourdomain.com
  • Password: the mailbox password

Step 2: PROTOCOL, Not MAILER_TYPE

This is the single biggest source of "I copied the config and nothing happened."

Gitea's 1.18 cycle renamed most of the [mailer] keys. The old names lingered as deprecated aliases for a few releases, then went away. Forgejo forked around that same point and kept the new names, so there's no divergence here — one block, both forks.

Old keyCurrent keyNote
MAILER_TYPEPROTOCOLValues: smtp, smtps, smtp+starttls, smtp+unix, sendmail, dummy
HOSTSMTP_ADDR + SMTP_PORTSplit into two keys. SMTP_ADDR takes no port.
IS_TLS_ENABLED = truePROTOCOL = smtpsTLS mode is now part of the protocol value
SKIP_VERIFYFORCE_TRUST_SERVER_CERTLeave this false
DISABLE_HELOENABLE_HELOInverted — flipping the name flips the meaning
USE_CERTIFICATEUSE_CLIENT_CERTClient certs, rarely needed

If you're pasting a config from a 2021 Stack Overflow answer, you're pasting three dead keys at once. Gitea won't always tell you. It'll just start up with mail disabled and act like everything's fine.

Step 3: Write the Gitea SMTP [mailer] Block

Here's the whole thing:

[mailer]
ENABLED = true
PROTOCOL = smtp+starttls
SMTP_ADDR = smtp.justemails.app
SMTP_PORT = 587
USER = git@yourdomain.com
PASSWD = `your-mailbox-password`
FROM = "Acme Git" [git@yourdomain.com](mailto:git@yourdomain.com)
SUBJECT_PREFIX = "[git] "

Three things in there will bite you.

SMTP_ADDR is hostname only. No port, no scheme. Writing smtp.justemails.app:587 there produces a DNS lookup failure that reads like the relay is down when it isn't. I've done this. Twice.

Those backticks around PASSWD are real syntax, not markdown. Gitea treats a backtick-wrapped value as literal, which protects passwords containing #, ;, or a trailing space. Without them, a # starts a comment and your password gets truncated at that character — and the failure reads as "authentication failed," which sends you hunting in entirely the wrong direction.

smtp+starttls on 587, not smtps. Port 587 negotiates TLS after connecting; 465 wraps the connection in TLS from the first byte. Mixing them gives you a handshake timeout.

Running in Docker and want it out of the file entirely? Both forks read env vars in a double-underscore format:

environment:
  - GITEA__mailer__ENABLED=true
  - GITEA__mailer__PROTOCOL=smtp+starttls
  - GITEA__mailer__SMTP_ADDR=smtp.justemails.app
  - GITEA__mailer__SMTP_PORT=587
  - GITEA__mailer__USER=git@yourdomain.com
  - GITEA__mailer__PASSWD__FILE=/run/secrets/smtp_pass

That __FILE suffix reads the value out of a file instead of the environment, which keeps the password from showing up in docker inspect. Forgejo accepts FORGEJO__ as the prefix and still honors GITEA__ for compatibility. Pick one and don't mix them in the same compose file.

Step 4: FROM vs ENVELOPE_FROM

Two different addresses doing two different jobs, and the naming does nothing to help. Not really Gitea's fault, mind you — it inherited the mess from SMTP, where the header sender and the envelope sender have been two unrelated things since the early eighties and everyone just learned to live with it.

FROM sets the header your users see in their mail client. It accepts a display name, hence the "Acme Git" [git@yourdomain.com](mailto:git@yourdomain.com) form.

ENVELOPE_FROM sets the SMTP-level sender — the MAIL FROM command, which becomes the Return-Path and is where bounces go. Leave it empty and Gitea reuses the address from FROM.

Leave it empty. That's the right answer for maybe 90% of self-hosted forges, and here's why: SPF validates the envelope sender, DMARC validates alignment between the envelope domain and the header domain. When both addresses are the same verified domain, you pass both without thinking about it. Set ENVELOPE_FROM to some other domain and you've quietly broken alignment — mail still sends, DMARC still fails, and you won't notice until someone's reset link lands in quarantine. If DMARC alignment isn't a phrase you've had to care about yet, the enforcement walkthrough covers what actually breaks and in what order.

The one real exception: set it to an empty angle-bracket pair (<>) if you want bounces suppressed entirely. Useful for a high-traffic instance where nobody reads the bounce mailbox anyway. Slightly antisocial, but I understand the impulse.

Step 5: Turn On the Mail That Actually Matters

Configuring the mailer doesn't switch on the features that use it. That's a separate section, and it's the step people skip:

[service]
REGISTER_EMAIL_CONFIRM = true
ENABLE_NOTIFY_MAIL = true

REGISTER_EMAIL_CONFIRM gates new accounts behind a verification click. On a public-facing instance, leave it off and you'll be pruning junk accounts by hand inside a month.

ENABLE_NOTIFY_MAIL is the one that sends issue and PR activity to watchers. It's off by default. So you can have a perfectly working mailer, send a successful test, and still get zero notifications, which is a genuinely confusing 20 minutes. Off-by-default is the wrong call here, if you ask me. Nobody configures a mailer hoping for fewer notifications.

Restart after editing. systemctl restart gitea, or docker compose restart.

Step 6: Test It — and What doctor Won't Tell You

Everyone recommends gitea doctor for this. So: run gitea doctor check --list and there is no mailer check in the output. Doctor validates database consistency, storage paths, hook scripts, and whether your config parses. None of that opens an SMTP connection.

It's still worth running once —

gitea doctor check --all

— because it confirms Gitea is reading the app.ini you think it's reading. Which, on a host that's been through a package upgrade or two, is not a given.

For the real mail test, log in as an admin and go to Site Administration → Configuration → Mailer Configuration. There's a field for a recipient address and a send button. Forgejo's admin panel puts it in the same place.

When the test lands, open the headers and look for:

Authentication-Results: ...
  spf=pass
  dkim=pass
  dmarc=pass

All three should say pass. JustEmails signs DKIM on its side automatically and auto-configures the SPF, DKIM, DMARC, and MTA-STS records, but the records still have to be live in DNS for receivers to verify anything — so if dkim comes back none, that's propagation, not config. Wait an hour.

Common Gitea SMTP Errors and Fixes

dial tcp: lookup smtp.justemails.app:587: no such host

The port ended up inside SMTP_ADDR. Move it to SMTP_PORT.

535 5.7.8 Authentication credentials invalid

Nine times out of ten it's the password, not the username. Check for a # or ; in it — without backticks around the value, everything after that character is treated as a comment. The other one: USER must be the full email address, not the local part.

Test email reports success, nothing arrives

The admin panel tells you the queue accepted the message and the SMTP conversation didn't error. Not that it was delivered. Usual cause: a FROM domain that isn't verified on your relay — auth succeeds, then the relay rejects the message. Check the rejected-message log in your JustEmails dashboard.

Mail works for resets but not for PR activity

ENABLE_NOTIFY_MAIL is still false. See Step 5. Also confirm the user is actually watching the repo — Gitea only notifies watchers and participants.

x509: certificate signed by unknown authority

Minimal container image with no CA bundle. apk add --no-cache ca-certificates on Alpine, apt-get install ca-certificates on Debian, then restart. Do not fix this with FORCE_TRUST_SERVER_CERT = true — that disables verification globally and turns your TLS connection into decoration. I flipped that flag once in a hurry, promised myself I'd fix it properly, and didn't for most of a year.

Everything worked, then stopped after an upgrade

Check the startup log for deprecation warnings. If you were still riding MAILER_TYPE as an alias, the release that removed it disabled your mailer silently.

Next Steps

The block above covers the forge itself. Four things I'd do next, roughly in that order:

  • Watch the queue. Gitea buffers outbound mail, so an unreachable relay means messages pile up rather than erroring loudly — gitea manager flush-queues forces a drain when you're debugging.
  • Point your other self-hosted services at the same mailbox. Uptime Kuma alerts take the same host and port, just from a different settings page.
  • Decide whether SMTP is the right transport long-term. For a forge, yes — it's what Gitea speaks natively. For application mail you write yourself, an HTTP API gives you delivery webhooks and message logs SMTP can't; the SMTP vs API breakdown covers where the line sits. The base plan includes 1,000 transactional API emails a month.
  • Track what your forge activity looks like over time with JustAnalytics, or wire forge events into sprint workflows with DevOS.

Frequently Asked Questions

What's the difference between PROTOCOL and MAILER_TYPE in Gitea?

They're the same setting, renamed. MAILER_TYPE was the original key; the 1.18 cycle replaced it with PROTOCOL, which accepts smtp, smtps, smtp+starttls, smtp+unix, sendmail, and dummy. The old keys survived as deprecated aliases for a few releases but you shouldn't count on them on 1.21 or newer. The same rename split HOST into SMTP_ADDR and SMTP_PORT, so if you're copying a config from an old blog post you'll be pasting three dead keys at once.

Why does Gitea say the test email sent but nothing arrives?

The admin panel reports whether Gitea handed the message to the queue and got a clean SMTP response — not whether it was delivered. Two usual causes: the FROM domain isn't verified on your relay, so the relay accepts the connection and then rejects the message, or the mail is sitting in Gitea's mailer queue. Check your relay's rejected-message log and the Gitea log with [log] LEVEL set to debug.

Do I need ENVELOPE_FROM in the Gitea mailer config?

Usually not. If you leave it empty, Gitea uses the address from FROM as the envelope sender, which is what you want when both are on the same verified domain — SPF checks the envelope, DMARC checks alignment between envelope and header, and identical addresses pass both. Set it only when the bounce address must differ from the display address, or set it to an empty angle-bracket pair to suppress bounce mail entirely.

Can gitea doctor test my SMTP settings?

No. Run gitea doctor check --list and you won't find a mailer check — doctor validates database consistency, storage paths, hook scripts, and config sanity, and none of those checks open an SMTP connection. Use it to confirm Gitea is reading the app.ini you think it's reading, then send a real test from Site Administration → Configuration → Mailer Configuration.


Try JustEmails

Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.

Start your 7-day free trial → · How it compares

gitea-smtpforgejo-mailerself-hosted-gitapp-ini-configsmtp-relaybuildinpublicsaasstudioaiworkforcebuildwithclaude

Related posts

Tutorials
Jenkins SMTP Setup: Build Notification Emails via JustEmails
14 min read
Tutorials
Zabbix and Netdata Alert Delivery Using JustEmails SMTP
12 min read
Tutorials
Discourse SMTP Setup: 6 Steps to Outbound and Reply-by-Email
12 min read