Tutorials11 min read

GitLab CI SMTP Notifications: 2026 Setup Guide

GitLab CI SMTP setup with DKIM. Stop missing failed builds.

By JustEmails Platform Team

The merge request got approved. The pipeline ran. And failed. Nobody noticed for two days because GitLab's email notification got filtered, the Slack integration was pointing at a channel nobody watches anymore, and the deploy script's built-in notification sent from a no-reply address that landed in spam.

I've lived this. More than once.

Look, GitLab has built-in notifications. They work fine for merge request comments and standard pipeline events. But they come from GitLab's servers, they look like GitLab's emails, and you don't control the content or recipients. When you need pipeline failures to land in your ops inbox — with your branding, from your domain, formatted how you want — you need to wire up your own SMTP.

By the end of this, you'll have GitLab CI jobs that send email notifications through JustEmails SMTP. Pipeline fails? You'll know. Deploy succeeds? Optional notification, your call. And the emails will actually arrive, because we're doing authentication properly.

What We're Building

A GitLab CI/CD configuration that:

  • Sends failure notifications when any job fails
  • Optionally sends success notifications on deploy completion
  • Uses authenticated SMTP through JustEmails
  • Stores credentials securely in CI/CD variables
  • Sends from your domain with proper SPF/DKIM

If you need to set up phone notifications for critical failures, VeloCalls integrates with CI pipelines for voice alerts. We'll use curl for SMTP (no extra dependencies) and I'll show an msmtp alternative if you prefer a cleaner syntax.

Prerequisites

  • A GitLab project with CI/CD enabled (GitLab.com or self-hosted, either works)
  • A JustEmails account with a domain configured — if you haven't done this, our custom domain setup guide walks through adding domains and verifying DNS
  • Your domain's SPF, DKIM, and DMARC records configured (JustEmails auto-configures these when you add a domain)
  • Maintainer access to the GitLab project (for adding CI/CD variables)

One thing: if you're self-hosting GitLab and your runners are behind a corporate firewall, confirm port 587 is open outbound. I've seen runners that could hit anything on 443 but literally nothing else. Spent two hours debugging "connection refused" before realizing it was a firewall rule from 2019 that nobody remembered existed. Usually not a problem on GitLab.com shared runners.

Step 1: Get Your SMTP Credentials

Log into justemails.app and grab:

  • Host: smtp.justemails.app
  • Port: 587 (STARTTLS)
  • Username: Full email address — something like ci@yourdomain.com
  • Password: The account password or an app-specific password

Create a dedicated mailbox for CI. Keeps things isolated. When you inevitably rotate passwords because someone pasted credentials into a ticket, you don't have to touch your other integrations. (This will happen. Plan for it.)

If you're also using the transactional API for application emails, the SMTP vs API comparison breaks down when each makes sense. For CI notifications, SMTP is usually simpler — no SDK, no dependencies.

Step 2: Add CI/CD Variables in GitLab

Never put passwords in .gitlab-ci.yml. Ever. Git history is forever, and someone will grep through it eventually.

Go to your project → Settings → CI/CD → Variables → Add variable.

Create three variables:

VariableValueSettings
SMTP_HOSTsmtp.justemails.appProtected, Masked
SMTP_USERci@yourdomain.comProtected, Masked
SMTP_PASSYour passwordProtected, Masked

Protected means these only get injected into pipelines running on protected branches (usually main or master). Good for production credentials. If you want these available on feature branches too, uncheck Protected — but think about whether you actually need deployment notifications on every branch.

Masked hides the value in job logs. If your script accidentally echoes $SMTP_PASS, GitLab replaces it with [MASKED]. Not foolproof, but catches mistakes. For teams tracking ad spend, tools like ClickzProtect also rely on secure CI/CD pipelines for fraud detection deployments.

For group-wide CI (multiple projects sharing credentials), add these at the group level instead: Group → Settings → CI/CD → Variables. All projects in the group inherit them.

Step 3: Create the Email Notification Job

Here's a minimal .gitlab-ci.yml that sends failure notifications:

stages:
  - build
  - test
  - deploy
  - notify

build:
  stage: build
  script:
    - echo "Building..."
    # Your actual build commands

test:
  stage: test
  script:
    - echo "Running tests..."
    # Your actual test commands

deploy:
  stage: deploy
  script:
    - echo "Deploying..."
    # Your actual deploy commands
  only:
    - main

notify-failure:
  stage: notify
  when: on_failure
  script:
    - |
      curl --ssl-reqd \
        --url "smtp://${SMTP_HOST}:587" \
        --user "${SMTP_USER}:${SMTP_PASS}" \
        --mail-from "${SMTP_USER}" \
        --mail-rcpt "team@yourdomain.com" \
        --upload-file - <<EOF
      From: CI Bot <${SMTP_USER}>
      To: team@yourdomain.com
      Subject: Pipeline FAILED: ${CI_PROJECT_NAME} (${CI_COMMIT_REF_NAME})

      Pipeline failed for ${CI_PROJECT_NAME} on branch ${CI_COMMIT_REF_NAME}.

      Commit: ${CI_COMMIT_SHA}
      Author: ${CI_COMMIT_AUTHOR}
      Message: ${CI_COMMIT_MESSAGE}

      View pipeline: ${CI_PIPELINE_URL}
      View job: ${CI_JOB_URL}
      EOF

The when: on_failure clause means this job only runs if any previous job in the pipeline failed. It runs after everything else, captures the failure, and fires off an email.

What those curl flags do:

  • --ssl-reqd — requires TLS, refuses to send over plaintext
  • --url "smtp://..." — the SMTP server with port (curl speaks SMTP natively since 7.20)
  • --user — SMTP authentication credentials
  • --mail-from / --mail-rcpt — envelope sender and recipient
  • --upload-file - — reads the email content from stdin (the heredoc)

The heredoc includes GitLab's predefined CI variables. CI_PIPELINE_URL gives you a direct link to the failed pipeline. CI_JOB_URL links to the specific failed job. Click, debug, fix. No hunting through GitLab's UI.

Step 4: Add Success Notifications (Optional)

Not everyone wants an email for every green build. Honestly, I find success emails annoying — they train you to ignore your inbox. I only notify on failures and first-success-after-failure. But if you want deploy confirmations:

notify-success:
  stage: notify
  when: on_success
  script:
    - |
      curl --ssl-reqd \
        --url "smtp://${SMTP_HOST}:587" \
        --user "${SMTP_USER}:${SMTP_PASS}" \
        --mail-from "${SMTP_USER}" \
        --mail-rcpt "team@yourdomain.com" \
        --upload-file - <<EOF
      From: CI Bot <${SMTP_USER}>
      To: team@yourdomain.com
      Subject: Deploy succeeded: ${CI_PROJECT_NAME} to ${CI_ENVIRONMENT_NAME}

      Deployment completed for ${CI_PROJECT_NAME}.

      Environment: ${CI_ENVIRONMENT_NAME}
      Branch: ${CI_COMMIT_REF_NAME}
      Commit: ${CI_COMMIT_SHORT_SHA}
      Author: ${CI_COMMIT_AUTHOR}

      Pipeline: ${CI_PIPELINE_URL}
      EOF
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

The rules section limits this to main branch only. You probably don't want 47 success emails from feature branches during active development.

Step 5: Alternative — Using msmtp for Cleaner Syntax

curl's SMTP support is fine, but the syntax is ugly. Like, really ugly. If you're doing anything more complex — HTML emails, attachments, multiple recipients with CC — msmtp is cleaner.

Install msmtp in your CI job and use a config:

notify-failure-msmtp:
  stage: notify
  when: on_failure
  image: alpine:latest
  before_script:
    - apk add --no-cache msmtp
    - |
      cat > /tmp/msmtprc <<EOF
      defaults
      auth on
      tls on
      tls_starttls on
      tls_certcheck off

      account default
      host ${SMTP_HOST}
      port 587
      from ${SMTP_USER}
      user ${SMTP_USER}
      password ${SMTP_PASS}
      EOF
  script:
    - |
      cat <<EOF | msmtp -C /tmp/msmtprc team@yourdomain.com
      Subject: Pipeline FAILED: ${CI_PROJECT_NAME}
      From: CI Bot <${SMTP_USER}>
      To: team@yourdomain.com

      Build failed. Check ${CI_PIPELINE_URL}
      EOF

The tls_certcheck off is only for Alpine's minimal CA bundle. On Ubuntu-based images, use tls_trust_file /etc/ssl/certs/ca-certificates.crt instead.

Yeah, I wish msmtp's TLS defaults were saner too. The documentation is thorough but somehow manages to make simple things confusing. Personal gripe.

Step 6: Verify Emails Actually Arrive

Trigger a failure on purpose. Easiest way:

test-failure:
  stage: test
  script:
    - exit 1  # Always fails

Push, wait for pipeline, check your inbox. If the email arrived, check authentication:

  1. Open the email in Gmail/Outlook
  2. View headers or "Show original"
  3. Look for SPF: PASS, DKIM: PASS, DMARC: PASS

All three should pass. If DKIM shows FAIL, your DNS records need the DKIM entry from JustEmails. Our DMARC enforcement guide covers the full verification process.

Common Errors and Fixes

Look, something will break. It always does. Here's the stuff I see most often.

curl: (67) Access denied: 535

Authentication failed. Check your SMTP_USER is the full email address (ci@yourdomain.com, not just ci). Check SMTP_PASS for trailing whitespace — copy fresh from your dashboard.

curl: (7) Failed to connect to smtp.justemails.app port 587

Network block. Shared GitLab.com runners have port 587 open, but self-hosted runners or enterprise setups might block it. Test with nc -zv smtp.justemails.app 587 in a job. If it times out, talk to your network team.

Emails arrive but go to spam

Missing authentication. Run mail-tester.com — send your test email to their address, get a deliverability report. Below 8/10 means work to do.

Usually it's a missing SPF include or DKIM record that hasn't propagated yet. DNS propagation is one of those things that's supposed to take "up to 48 hours" but somehow always takes exactly as long as your patience lasts. For browser-based email testing and debugging, JustBrowser sessions can help inspect raw email headers.

Variable not set / empty SMTP_HOST

The variable exists but isn't being injected. Check if Protected is checked — if so, your pipeline must be running on a protected branch. Or the variable scope doesn't include your branch. Temporarily uncheck Protected to debug.

curl: (60) SSL certificate problem

TLS verification failed. On some minimal Docker images, the CA bundle is missing. Add --insecure temporarily to confirm it's a cert issue (don't leave this in production). Proper fix is to install ca-certificates in your image.

Advanced: Conditional Recipients

Different people care about different failures. Here's a pattern that routes backend test failures to the backend team and frontend failures to frontend:

notify-backend-failure:
  stage: notify
  when: on_failure
  rules:
    - if: $CI_JOB_NAME =~ /backend/
  script:
    - # email to backend-team@yourdomain.com

notify-frontend-failure:
  stage: notify
  when: on_failure
  rules:
    - if: $CI_JOB_NAME =~ /frontend/
  script:
    - # email to frontend-team@yourdomain.com

The rules match against the failed job's name. Regex patterns work. Gets noisy if you have dozens of jobs, but for smaller pipelines it's a quick way to route alerts. (I tried building a fancy routing engine once. Spent a week on it. Turned out regex matching on job names did 90% of what I needed. Lesson learned.)

Next Steps

Pipeline notifications are wired up. Now what?

  • Add Slack/Discord fallback — email for durability, chat for immediacy. GitLab has native integrations; the SMTP path here is for when you need email specifically
  • Monitor deliverabilityGoogle Postmaster Tools tracks your domain's reputation. Worth checking monthly
  • Centralize multi-project alerts — if you're running 20 GitLab projects, group-level variables and a shared notify template in a CI template repo save repetition
  • Check out DevOS for broader CI/CD automation if you're doing this across multiple platforms

If you're measuring DORA metrics, pipeline emails are the raw signal. JustAnalytics can help track pipeline performance trends over time alongside your other engineering metrics.

Frequently Asked Questions

How do I send email notifications from GitLab CI pipelines?

Add a job that uses curl, msmtp, or a sendmail wrapper to dispatch email through an SMTP server. Store your SMTP host, username, and password as protected CI/CD variables in your project or group settings. Reference them in your .gitlab-ci.yml and trigger the job on pipeline success, failure, or both using rules or when: conditions.

Why are my GitLab CI notification emails going to spam?

The sending domain lacks proper authentication. SPF, DKIM, and DMARC records must be configured for the domain in your From address. If you're sending from ci@yourdomain.com, that domain needs DNS records pointing to your SMTP provider. JustEmails auto-configures DKIM signing, but you still need to verify the records are in place.

Should I use GitLab's built-in email notifications or custom SMTP?

GitLab's built-in notifications work for standard events like merge request comments and pipeline status — but they come from GitLab's servers, not your domain. Custom SMTP lets you send from your own domain, change what the message says, pick recipients per-job, and plug pipeline events into whatever alerting you've already got running. Use built-in for standard stuff, custom SMTP when you want control.

How do I store SMTP credentials securely in GitLab CI?

Go to your project or group Settings → CI/CD → Variables. Add SMTP_HOST, SMTP_USER, and SMTP_PASS as protected, masked variables. Protected means they're only available to pipelines on protected branches. Masked means the values are hidden in job logs. Reference them in your pipeline as $SMTP_HOST, $SMTP_USER, and $SMTP_PASS.


Try JustEmails

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

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

gitlab-ci-smtppipeline-notificationsemail-authenticationdevops-emailci-cd-alertsbuildinpublicsaasstudioaiworkforcebuildwithclaude

Related posts