Heim  >  Artikel  >  Backend-Entwicklung  >  Erstellen Sie Ihren eigenen SMTP-Server in Go

Erstellen Sie Ihren eigenen SMTP-Server in Go

PHPz
PHPzOriginal
2024-07-28 02:01:031065Durchsuche

Build Your Own SMTP Server in Go

Bei Valyent entwickeln wir Open-Source-Software für Entwickler.

Im Rahmen dieser Mission entwickeln wir derzeit Ferdinand, unseren E-Mail-Versanddienst für Entwickler (derzeit in der Alpha-Phase).

Die E-Mail-Infrastruktur basiert auf mehreren Schlüsselprotokollen, wobei das wichtigste folgendes ist:

  1. SMTP (Simple Mail Transfer Protocol): Wird zum Senden und Empfangen von E-Mails zwischen Mailservern verwendet.
  2. IMAP (Internet Message Access Protocol): Ermöglicht Benutzern das Lesen und Verwalten von E-Mails direkt vom Server.
  3. POP3 (Post Office Protocol Version 3): Lädt E-Mails vom Server auf das lokale Gerät herunter und entfernt sie normalerweise vom Server.

Im heutigen Artikel konzentrieren wir uns auf den Aufbau unseres eigenen SMTP-Ausgangsservers, der den Ansatz widerspiegelt, den wir mit Ferdinand gewählt haben. Dadurch erlangen wir ein tiefes Verständnis der wichtigsten Komponente der E-Mail-Versandinfrastruktur.

„Was ich nicht erschaffen kann, verstehe ich nicht.“

– Richard Feynman

Indem Sie einen ausgehenden SMTP-Server von Grund auf neu erstellen, können Sie einen Einblick in die E-Mail-Zustellung gewinnen, den die meisten Entwickler nie erreichen.

Um fortzufahren, werden wir die Programmiersprache Go zusammen mit den tollen Mail-Bibliotheken von Simon Ser verwenden. Wir entmystifizieren den Prozess, zeigen Ihnen, wie Sie E-Mails an andere Server senden und erklären sogar Schlüsselkonzepte wie SPF, DKIM und DMARC, die die Zustellbarkeit gewährleisten.

Am Ende verfügen Sie zumindest über ein tieferes Verständnis der E-Mail-Infrastruktur, auch wenn Sie noch keinen produktionsbereiten SMTP-Server haben.

SMTP verstehen: Die Grundlagen

Bevor wir uns mit dem Code befassen, schauen wir uns an, was SMTP ist und wie es funktioniert. SMTP (Simple Mail Transfer Protocol) ist das Standardprotokoll zum Senden von E-Mails über das Internet. Es handelt sich um ein relativ einfaches, textbasiertes Protokoll, das nach einem Client-Server-Modell arbeitet.

SMTP-Befehle

Das SMTP-Protokoll verwendet Befehle. Jeder Befehl in SMTP dient einem bestimmten Zweck im E-Mail-Übertragungsprozess. Sie ermöglichen es Servern, sich vorzustellen, Absender und Empfänger festzulegen, den eigentlichen E-Mail-Inhalt zu übertragen und die gesamte Kommunikationssitzung zu verwalten. Stellen Sie sich diese Befehle als eine strukturierte Konversation zwischen zwei E-Mail-Servern vor, wobei jeder Befehl eine bestimmte Aussage oder Frage in dieser Konversation darstellt.

Wenn Sie einen SMTP-Server erstellen, erstellen Sie im Wesentlichen ein Programm, das diese Sprache fließend sprechen kann, eingehende Befehle interpretiert und angemessen reagiert sowie beim Senden von E-Mails die richtigen Befehle ausgibt.

Lassen Sie uns die wichtigsten SMTP-Befehle untersuchen, um zu sehen, wie sich dieses Gespräch entwickelt:

  • HELO/EHLO (Hallo): Dieser Befehl initiiert die SMTP-Konversation. EHLO ist die erweiterte SMTP-Version, die zusätzliche Funktionen unterstützt. Die Syntax ist HELO-Domäne oder EHLO-Domäne. Zum Beispiel: EHLO example.com.
  • MAIL FROM: Dieser Befehl gibt die E-Mail-Adresse des Absenders an und startet eine neue E-Mail-Transaktion. Es verwendet die Syntax MAIL FROM:. Ein Beispiel wäre MAIL FROM:.
  • RCPT TO: Dieser Befehl wird zur Angabe der E-Mail-Adresse des Empfängers verwendet und kann mehrmals für mehrere Empfänger verwendet werden. Die Syntax lautet RCPT TO:. Zum Beispiel: RCPT TO:.
  • DATA: Dieser Befehl gibt den Anfang des Nachrichteninhalts an. Es endet mit einer Zeile, die nur einen einzigen Punkt (.) enthält. Nach dem DATA-Befehl geben Sie den Nachrichteninhalt ein. Zum Beispiel:
DATA
From: john@example.com
To: jane@example.com
Subject: Hello

This is the body of the email.
.
  • QUIT: Dieser einfache Befehl beendet die SMTP-Sitzung. Seine Syntax ist einfach QUIT.
  • RSET (Zurücksetzen): Der RSET-Befehl bricht die aktuelle E-Mail-Transaktion ab, hält aber die Verbindung offen. Dies ist nützlich, um von vorne zu beginnen, ohne eine neue Verbindung herzustellen. Die Syntax ist einfach RSET.
  • AUTH (Authentifizierung): Dieser Befehl wird zur Authentifizierung des Clients gegenüber dem Server verwendet und unterstützt verschiedene Authentifizierungsmechanismen. Die Syntax ist der AUTH-Mechanismus, zum Beispiel: AUTH LOGIN.

Eine typische SMTP-Konversation könnte so aussehen:

C: EHLO client.example.com
S: 250-smtp.example.com Hello client.example.com
S: 250-SIZE 14680064
S: 250-AUTH LOGIN PLAIN
S: 250 HELP

C: MAIL FROM:<sender@example.com>
S: 250 OK

C: RCPT TO:<recipient@example.com>
S: 250 OK

C: DATA
S: 354 Start mail input; end with <CRLF>.<CRLF>

C: From: sender@example.com
C: To: recipient@example.com
C: Subject: Test Email
C:
C: This is a test email.
C: .

S: 250 OK: queued as 12345

C: QUIT
S: 221 Bye

Authentifizierung in SMTP

Authentifizierung ist ein entscheidender Aspekt von SMTP, insbesondere für ausgehende E-Mail-Server. Es hilft, unbefugte Nutzung des Servers zu verhindern und Spam zu reduzieren. In SMTP werden mehrere Authentifizierungsmethoden verwendet:

  1. PLAIN: This is a simple authentication method where the username and password are sent in clear text. It should only be used over encrypted connections.
  2. LOGIN: Similar to PLAIN, but the username and password are sent in separate commands.
  3. CRAM-MD5: This method uses a challenge-response mechanism to avoid sending the password in clear text.
  4. OAUTH2: This method allows the use of OAuth 2.0 tokens for authentication.

Here's an example of how PLAIN authentication looks in an SMTP conversation:

C: EHLO example.com
S: 250-STARTTLS
S: 250 AUTH PLAIN LOGIN
C: AUTH PLAIN AGVtYWlsQGV4YW1wbGUuY29tAHBhc3N3b3Jk
S: 235 2.7.0 Authentication successful

In this example, AGVtYWlsQGV4YW1wbGUuY29tAHBhc3N3b3Jk is the base64-encoded version of \0email@example.com\0password.

When implementing authentication in your SMTP server, you'll need to:

  1. Advertise supported authentication methods in response to the EHLO command.
  2. Implement handlers for the AUTH command that can process the chosen authentication method.
  3. Verify the provided credentials against your user database.
  4. Maintain the authenticated state for the duration of the SMTP session.

Now, let's move on to implementing these concepts in our Go SMTP server.

Achieving deliverability: DKIM, SPF, DMARC

Imagine sending a letter through the postal service without a return address or an official stamp. It might reach its destination, but there's a good chance it'll end up in the "suspicious mail" pile. In the digital world of email, we face a similar challenge.

How do we ensure our emails aren't just sent, but actually delivered and trusted?

Enter the holy trinity of email authentication: DKIM, SPF, and DMARC.

DKIM: Your Email's Digital Signature

DKIM (DomainKeys Identified Mail) is like a wax seal on a medieval letter. It proves the email hasn't been tampered with during transit.

How it works:

  • Your email server adds a digital signature to every outgoing email.
  • The receiving server checks this signature against a public key published in your DNS records.
  • If the signature is valid, the email passes the DKIM check.

Think of it as your email's passport, stamped and verified at each checkpoint.

Example DKIM DNS Record:

<selector>._domainkey.<domain>.<tld>. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3QEKyU1fSma0axspqYK5iAj+54lsAg4qRRCnpKK68hawSd8zpsDz77ntGCR0X2mHVvkHbX6dX...oIDAQAB"

Here, 'selector' is a unique identifier for this DKIM key, and the long string is your public key.

SPF: The Guest List for Your Domain's Party

SPF (Sender Policy Framework) is like the bouncer at an exclusive club. It specifies which email servers are allowed to send emails on behalf of your domain.

How it works:

  • You publish a list of authorized IP addresses in your DNS records.
  • When an email arrives claiming to be from your domain, the receiving server checks if it came from an IP on your list.
  • If it matches, the email passes the SPF check.

It's like saying, "If the email didn't come from one of these guys, it's not with us!"

Example SPF DNS Record:

<domain>.<tld>. IN TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.google.com ~all"

This record says:

  • Emails can come from IP addresses in the range 192.0.2.0 to 192.0.2.255.
  • Emails can also come from servers specified in Google's SPF record.
  • The ~all means to soft-fail emails from other sources (treat as suspicious but don't reject).

DMARC: The Rule Maker and Enforcer

DMARC (Domain-based Message Authentication, Reporting & Conformance) is the wise judge that decides what happens to emails that fail DKIM or SPF checks.

How it works:

  • You set a policy in your DNS records specifying how to handle emails that fail authentication.
  • Options range from "let it through anyway" to "reject it outright."
  • DMARC also provides reports on email authentication results, helping you monitor and improve your email security.

Think of DMARC as your email bouncer's rulebook and incident report.

Example DMARC DNS Record:

_dmarc.<domain>.<tld>. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@<domain>.<tld>"

This record says:

  • If an email fails DKIM and SPF checks, quarantine it (typically send to spam folder).
  • Send aggregate reports about email authentication results to dmarc-reports@example.com.

Why This Trinity Matters

Together, DKIM, SPF, and DMARC form a powerful shield against email spoofing and phishing. They tell receiving servers, "This email is really from us, sent by someone we trust, and here's what to do if something seems fishy."

Implementing this trinity not only improves your email deliverability but also protects your domain's reputation. It's like having a state-of-the-art security system for your email infrastructure.

As we build our SMTP server, keeping these authentication methods in mind will be crucial for ensuring our emails don't just get sent, but actually reach their destination and are trusted when they arrive. Remember, when implementing these records on a production domain, start with permissive policies and gradually tighten them as you confirm everything is working correctly.

Building the SMTP Server with Go

1. Project Initialization

First, let's create a new directory for our project and initialize a Go module:

mkdir go-smtp-server
cd go-smtp-server
go mod init github.com/yourusername/go-smtp-server

2. Installing Dependencies

We'll need a few dependencies for our SMTP server. Run the following commands:

go get github.com/emersion/go-smtp
go get github.com/emersion/go-sasl
go get github.com/emersion/go-msgauth

3. Basic SMTP Server Setup

  1. Create a new file named main.go and add the following code:
package main

import (
    "log"
    "time"
    "io"

    "github.com/emersion/go-smtp"
)

func main() {
    s := smtp.NewServer(&Backend{})

    s.Addr = ":2525"
    s.Domain = "localhost"
    s.WriteTimeout = 10 * time.Second
    s.ReadTimeout = 10 * time.Second
    s.MaxMessageBytes = 1024 * 1024
    s.MaxRecipients = 50
    s.AllowInsecureAuth = true

    log.Println("Starting server at", s.Addr)
    if err := s.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

// Backend implements SMTP server methods.
type Backend struct{}

func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) {
    return &Session{}, nil
}

// A Session is returned after EHLO.
type Session struct{}

// We'll implement the Session methods next

This creates an SMTP server, listening on the 2525 port, a convenient choice for development purposes, since this port doesn’t require administrative privileges, unlike the standard ports 25 (standard SMTP), 465 (TLS), 587 (STARTTLS).

  1. Implementing EHLO/HELO

The EHLO/HELO command is handled automatically by the go-smtp library. We don't need to implement it ourselves.

  1. Implementing MAIL FROM

Add this method to the Session struct:

func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
    fmt.Println("Mail from:", from) s.From = from
    return nil
}

This method is called when the server receives a MAIL FROM command. It logs the sender's address and stores it in the session.

  1. Implementing RCPT TO

Add this method to the Session struct:

func (s *Session) Rcpt(to string) error {
    fmt.Println("Rcpt to:", to)
    s.To = append(s.To, to)
    return nil
}

This method is called for each RCPT TO command. It logs the recipient's address and adds it to the list of recipients for this session.

  1. Implementing DATA

Add this method to the Session struct:

import (
    "fmt"
    "io"
)

func (s *Session) Data(r io.Reader) error {
    if b, err := io.ReadAll(r); err != nil {
        return err
    } else {
        fmt.Println("Received message:", string(b))

        // Here you would typically process the email
        return nil
    }
}

This method is called when the server receives the DATA command. It reads the entire email message and logs it. In a real server, you would process the email here.

  1. Implementing AUTH

Add this method to the Session struct:

func (s *Session) AuthPlain(username, password string) error {
    if username != "testuser" || password != "testpass" {
        return fmt.Errorf("Invalid username or password")
    }

    return nil
}

This implements a basic authentication mechanism. Note that this is for demonstration purposes only and should not be used in production.

  1. Implementing RSET

Add this method to the Session struct:

func (s *Session) Reset() {
    s.From = "" s.To = []string{}
}

This method is called when the server receives a RSET command. It resets the session state.

  1. Implementing QUIT

Add this method to the Session struct:

func (s *Session) Logout() error {
    return nil
}

This method is called when the server receives a QUIT command. In this simple implementation, we don't need to do anything special.

  1. Sending Emails: MX Lookup, Port Selection, and DKIM Signing

Once we've received and processed an email, the next step is to send it to its destination. This involves two key steps: finding the recipient's mail server using MX (Mail Exchanger) records, and attempting to send the email using standard SMTP ports.

First, let's add a function to look up MX records:

import "net"

func lookupMX(domain string) ([]*net.MX, error) {
    mxRecords, err := net.LookupMX(domain)
    if err != nil {
        return nil, fmt.Errorf("Error looking up MX records: %v", err)
    }

    return mxRecords, nil
}

Next, let's create a function that attempts to send an email using different ports:

import (
    "crypto/tls"
    "net/smtp"
    "strings"
)

func sendMail(from string, to string, data []byte) error {
    domain := strings.Split(to, "@")[1]

    mxRecords, err := lookupMX(domain)
    if err != nil {
        return err
    }

    for _, mx := range mxRecords {
        host := mx.Host

        for _, port := range []int{25, 587, 465} {
            address := fmt.Sprintf("%s:%d", host, port)

            var c *smtp.Client

            var err error

            switch port {
            case 465:
                // SMTPS
                tlsConfig := &tls.Config{ServerName: host}
                conn, err := tls.Dial("tcp", address, tlsConfig)
                if err != nil {
                    continue
                }

                c, err = smtp.NewClient(conn, host)

            case 25, 587:
                // SMTP or SMTP with STARTTLS
                c, err = smtp.Dial(address)
                if err != nil {
                    continue
                }

                if port == 587 {
                    if err = c.StartTLS(&tls.Config{ServerName: host}); err != nil {
                        c.Close()
                        continue
                    }
                }
            }

            if err != nil {
                continue
            }

            // SMTP conversation
            if err = c.Mail(from); err != nil {
                c.Close()
                continue
            }

            if err = c.Rcpt(to); err != nil {
                c.Close()
                continue
            }

            w, err := c.Data()
            if err != nil {
                c.Close()
                continue
            }

            if _, err := w.Write(data); err != nil {
                c.Close()
                continue
            }

            err = w.Close()
            if err != nil {
                c.Close()
                continue
            }

            c.Quit()

            return nil
        }
    }

    return fmt.Errorf("Failed to send email to %s", to)
}

This function does the following:

  • Looks up the MX records for the recipient's domain.
  • For each MX record, it tries to connect using ports 25, 587, and 465 in that order.
  • It uses the appropriate connection method for each port:
    • Port 25: Plain SMTP
    • Port 587: SMTP with STARTTLS
    • Port 465: SMTPS (SMTP over TLS)
  • If a connection is successful, it attempts to send the email using the SMTP protocol.
  • If the email is sent successfully, it returns. Otherwise, it tries the next port or MX record.

Now, let's modify our Data method in the Session struct to use this new sendMail function:

func (s *Session) Data(r io.Reader) error {
    if data, err := io.ReadAll(r); err != nil {
        return err
    } else {
        fmt.Println("Received message:", string(data))
        for _, recipient := range s.To {
            if err := sendMail(s.From, recipient, data); err != nil {
                fmt.Printf("Failed to send email to %s: %v", recipient, err)
            } else {
                fmt.Printf("Email sent successfully to %s", recipient)
            }

        }

        return nil
    }
}

This implementation will attempt to send the received email to each recipient using the appropriate mail server and port.

Now, let's add DKIM signing to our email sending process. First, we need to import the necessary packages and set up our DKIM options:

import (
    // ... other imports ...
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "github.com/emersion/go-msgauth/dkim"
)

// Load your DKIM private key
var dkimPrivateKey *rsa.PrivateKey

func init() {
    // Load your DKIM private key from a file
    privateKeyPEM, err := ioutil.ReadFile("path/to/your/private_key.pem")
    if err != nil {
        log.Fatalf("Failed to read private key: %v", err)
    }

    block, _ := pem.Decode(privateKeyPEM)
    if block == nil {
        log.Fatalf("Failed to parse PEM block containing the private key")
    }

    privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        log.Fatalf("Failed to parse private key: %v", err)
    }

    dkimPrivateKey = privateKey
}

// DKIM options
var dkimOptions = &dkim.SignOptions{
    Domain: "example.com",
    Selector: "default",
    Signer: dkimPrivateKey,
}

Next, let's modify our sendMail function to include DKIM signing:

func sendMail(from string, to string, data []byte) error {
    // ... [previous MX lookup code] ...

    for _, mx := range mxRecords {
        host := mx.Host
        for _, port := range []int{25, 587, 465} {
            // ... [previous connection code] ...

            // DKIM sign the message
            var b bytes.Buffer
            if err := dkim.Sign(&b, bytes.NewReader(data), dkimOptions); err != nil {
                return fmt.Errorf("Failed to sign email with DKIM: %v", err)
            }
            signedData := b.Bytes()

            // SMTP conversation
            if err = c.Mail(from); err != nil {
                c.Close()
                continue
            }
            if err = c.Rcpt(to); err != nil {
                c.Close()
                continue
            }
            w, err := c.Data()
            if err != nil {
                c.Close()
                continue
            }
            _, err = w.Write(signedData) // Use the DKIM signed message
            if err != nil {
                c.Close()
                continue
            }
            err = w.Close()
            if err != nil {
                c.Close()
                continue
            }
            c.Quit()
            return nil
        }
    }

    return fmt.Errorf("Failed to send email to %s", to)
}

In this updated sendMail function:

  • We sign the email data with DKIM before sending it.
  • We use the signed data (signedData) when writing to the SMTP connection.

This implementation will add a DKIM signature to your outgoing emails, which will help improve deliverability and authenticity of your emails.

Denken Sie daran, „path/to/your/private_key.pem“ durch den tatsächlichen Pfad zu Ihrem privaten DKIM-Schlüssel zu ersetzen und die Domäne und den Selektor in dkimOptions so zu aktualisieren, dass sie mit Ihrem DKIM-DNS-Eintrag übereinstimmen.

  1. Überlegungen und nächste Schritte

Während diese Implementierung einen grundlegenden funktionierenden SMTP-Server bereitstellt, der E-Mails empfangen und senden kann, gibt es mehrere wichtige Überlegungen für einen produktionsbereiten Server:

  • Ratenbegrenzung: Implementieren Sie eine Ratenbegrenzung, um Missbrauch zu verhindern und vor E-Mail-Bombing zu schützen.
  • Spam-Prävention: Implementieren Sie Maßnahmen, um zu verhindern, dass Ihr Server zum Versenden von Spam missbraucht wird.
  • Fehlerbehandlung: Verbessern Sie die Fehlerbehandlung und -protokollierung für besseres Debuggen und Überwachen.
  • Warteschlangenverwaltung: Implementieren Sie ein Warteschlangensystem für Wiederholungslogik, wenn E-Mails nicht gesendet werden können.

Abschluss

Wir hoffen, dass Sie durch die Lektüre dieses Beitrags viel gelernt haben. Um mehr über das Versenden von E-Mails zu erfahren, schauen Sie sich gerne das GitHub-Repository von Ferdinand an und erkunden Sie den Code.

Das obige ist der detaillierte Inhalt vonErstellen Sie Ihren eigenen SMTP-Server in Go. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:Produzenten-Konsumenten-MusterNächster Artikel:Produzenten-Konsumenten-Muster