Autoreply für Postfächer mittels Docker

Viele Anbieter von Maildiensten (bspw. IONOS, 1&1, T-Online, etc.) bieten nur eingeschränkte Möglichkeiten für einen Autoreply von E-Mails. HTML in Texten, das Einfügen von Logos oder das Verändern des Betreffs sind meist nicht möglich. Mittels eines einfach Docker-Containers kann man dies selbst erledigen, voll anpassbar an das gewünschte Design (mit etwas HTML-Kenntnis). Der Container konnektiert sich via IMAP auf das gewünschte Postfach.

Vorbereitung

Möchte man die Daten persistent in einem Verzeichnis speichern, sollte dieses vorab angelegt werden, bspw. in:

/docker/autoreply

In diesem Verzeichnis erzeugt man folgende Dateien:

  • autoreply.py
  • docker-compose.yml
  • Dockerfile
  • manage_db
  • reply.html
  • logo.png (gewünschtes Logo oder Bild)
  • requirements.txt

Dockerfile

📄
Dockerfile
Copy to clipboard
FROM python:3.11-slim

WORKDIR /app

# Requirements
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

# Copy Autoreply + HTML + Logo + Admin-Tool
COPY autoreply.py reply.html logo.png manage_db.py /app/

VOLUME ["/app/logs"]

CMD ["python", "-u", "autoreply.py"]

requirements.txt

📄
requirements.txt
Copy to clipboard
imap-tools==1.5.0

autoreply.py

Anmerkungen zur autoreply.py. Die nachfolgenden Werte können frei angepasst werden:

  • SMTP_PORT (587 für TLS, 465 für SSL)
  • über POLL_SECONDS wird der Abfrageintervall eingestellt (alle 60 Sekunden)
  • REBOUND_HOURS schickt alle 24h eine Antwort an den Absender. Zusätzlich dazu wird eine SQLite-DB angelegt, welche in /app/logs/replied.db die gesendeten Antworten speichert
  • IGNORE_ADDRS kann mit Adressmustern gefüllt werden, an die keine Antwort gesendeten werden kann (Vermeidung von do-not-reply-Ping-Pong)
  • Für die Verwendung eines Logos wird dieses in base64 encodiert

Der Container „fasst“ Mails nicht an, setzt diese also nicht als „Gelesen“.

🐍
autoreply.py
Copy to clipboard
#!/usr/bin/env python3
import os, time, smtplib, ssl, base64, sqlite3
from imap_tools import MailBox, AND
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from datetime import datetime, timedelta, timezone

# --- ENV Variablen ---
imap_host = os.getenv("IMAP_HOST")
imap_user = os.getenv("IMAP_USER")
imap_pass = os.getenv("IMAP_PASS")
smtp_host = os.getenv("SMTP_HOST")
smtp_port = int(os.getenv("SMTP_PORT", "587"))
smtp_user = os.getenv("SMTP_USER", imap_user)
smtp_pass = os.getenv("SMTP_PASS", imap_pass)
poll_seconds = int(os.getenv("POLL_SECONDS", "60"))

# Rebound Zeitraum (Stunden)
rebound_hours = int(os.getenv("REBOUND_HOURS", "24"))

# Ignorier-Muster für Absender
ignore_patterns = [p.strip().lower() for p in os.getenv(
    "IGNORE_ADDRS", "no-reply@,noreply@,donotreply@"
).split(",")]

ctx = ssl.create_default_context()

# --- SQLite DB für Rebound ---
db_path = "/app/logs/replied.db"
os.makedirs("/app/logs", exist_ok=True)
conn = sqlite3.connect(db_path, check_same_thread=False)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS replied (
    addr TEXT PRIMARY KEY,
    last_reply TIMESTAMP
)
""")
conn.commit()


def log(msg: str):
    print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True)


def already_replied(addr: str) -> bool:
    c.execute("SELECT last_reply FROM replied WHERE addr = ?", (addr,))
    row = c.fetchone()
    if row:
        last = datetime.fromisoformat(row[0])
        if datetime.now() - last < timedelta(hours=rebound_hours):
            return True
    return False


def mark_replied(addr: str):
    c.execute("INSERT OR REPLACE INTO replied (addr, last_reply) VALUES (?, ?)",
              (addr, datetime.now().isoformat()))
    conn.commit()


def send_reply(to_addr, original_subject):
    # Blacklist-Absender ignorieren
    low = to_addr.lower()
    if any(p in low for p in ignore_patterns):
        log(f"[Autoreply] Ignoriere {to_addr} (Blacklist-Match)")
        return

    if already_replied(to_addr):
        log(f"[Autoreply] Überspringe {to_addr}, bereits innerhalb {rebound_hours}h beantwortet")
        return

    msg = MIMEMultipart("alternative")
    msg["Subject"] = "Ihre Mail wurde empfangen"
    msg["From"] = smtp_user
    msg["To"] = to_addr

    # Inline-Logo
    with open("logo.png", "rb") as f:
        logo_b64 = base64.b64encode(f.read()).decode()
    with open("reply.html", "r", encoding="utf-8") as f:
        html = f.read().replace("{{INLINE_LOGO}}", logo_b64)
    msg.attach(MIMEText(html, "html"))

    def _send():
        if smtp_port == 465:
            with smtplib.SMTP_SSL(smtp_host, smtp_port, context=ctx, timeout=20) as s:
                s.set_debuglevel(1)
                s.ehlo()
                s.login(smtp_user, smtp_pass)
                s.sendmail(smtp_user, [to_addr], msg.as_string())
        else:
            with smtplib.SMTP(smtp_host, smtp_port, timeout=20) as s:
                s.set_debuglevel(1)
                s.ehlo()
                s.starttls(context=ctx)
                s.ehlo()
                s.login(smtp_user, smtp_pass)
                s.sendmail(smtp_user, [to_addr], msg.as_string())

    try:
        _send()
        log(f"[Autoreply] Antwort gesendet an {to_addr}")
        mark_replied(to_addr)
    except (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError) as e:
        log(f"[Autoreply] SMTP Verbindungsfehler ({e}), Retry …")
        try:
            time.sleep(2)
            _send()
            log(f"[Autoreply] Antwort gesendet an {to_addr} (Retry)")
            mark_replied(to_addr)
        except Exception as e2:
            log(f"[Autoreply] SMTP Fehler nach Retry: {e2}")
    except Exception as e:
        log(f"[Autoreply] SMTP Fehler: {e}")


def check_mail():
    try:
        log("Verbinde mit IMAP...")
        with MailBox(imap_host).login(imap_user, imap_pass, initial_folder="INBOX") as mailbox:
            log("IMAP Login OK, prüfe neue Mails...")

            cutoff = datetime.now(timezone.utc) - timedelta(seconds=poll_seconds*2)

            for msg in mailbox.fetch(AND(seen=False), mark_seen=False):
                if msg.date < cutoff:
                    continue  # alte ungelesene überspringen

                # Sicherheitsmaßnahme: falls SEEN gesetzt → zurücksetzen
                try:
                    mailbox.flag(msg.uid, ['\\Seen'], False)
                except Exception as e:
                    log(f"[Autoreply] Warnung: SEEN-Flag konnte nicht zurückgesetzt werden: {e}")

                log(f"Neue Mail von {msg.from_} mit Betreff: {msg.subject}")
                send_reply(msg.from_, msg.subject)
    except Exception as e:
        log(f"IMAP Fehler: {e}")


if __name__ == "__main__":
    log(f"Starte Auto-Reply Service für {imap_user}")
    log(f"Ignoriere Absender-Muster: {ignore_patterns}")
    while True:
        check_mail()
        time.sleep(poll_seconds)

manage_db.py

Hierbei handelt es sich um ein Helper-Tool, welches im laufenden Container ausgeführt werden kann:

  • python manage_db.py list → zeigt alle gespeicherten Adressen und deren Zeitstempel
  • python manage_db.py clear → leert die komplette Tabelle
  • python manage_db.py remove <mail> → löscht gezielt einen Eintrag

Beispiele

Wenn der Container bspw. autoreply heißt:

Alle Einträge anzeigen

				
					docker exec -it autoreply python manage_db.py list
				
			

Alle Einträge löschen

				
					docker exec -it autoreply python manage_db.py clear
				
			

Einen bestimmten Eintrag löschen

				
					docker exec -it autoreply python manage_db.py remove user@example.com
				
			
🐍
manage_db.py
Copy to clipboard
#!/usr/bin/env python3
import sqlite3, os, sys
from datetime import datetime

db_path = "/app/logs/replied.db"

def list_entries():
    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    rows = c.execute("SELECT addr, last_reply FROM replied").fetchall()
    conn.close()

    if not rows:
        print("📭 Keine Einträge gefunden.")
    else:
        print("📋 Replied-Adressen:")
        for addr, ts in rows:
            print(f"- {addr} (zuletzt: {ts})")

def clear_entries():
    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    c.execute("DELETE FROM replied")
    conn.commit()
    conn.close()
    print("🗑️ Alle Einträge gelöscht.")

def remove_entry(addr):
    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    c.execute("DELETE FROM replied WHERE addr = ?", (addr,))
    conn.commit()
    conn.close()
    print(f"🗑️ Eintrag für {addr} gelöscht.")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("⚙️ Nutzung:")
        print("  python manage_db.py list         # alle Einträge anzeigen")
        print("  python manage_db.py clear        # alle Einträge löschen")
        print("  python manage_db.py remove <mail> # spezifische Adresse löschen")
        sys.exit(1)

    cmd = sys.argv[1].lower()
    if cmd == "list":
        list_entries()
    elif cmd == "clear":
        clear_entries()
    elif cmd == "remove" and len(sys.argv) == 3:
        remove_entry(sys.argv[2])
    else:
        print("❌ Ungültiger Befehl.")
        sys.exit(1)

docker-compose.yml

In der docker-compose.yml werden die gängigen IMAP & SMTP-Werte eingetragen (Username ist üblicherweise die volle Mailadresse, POLL_SECONDS sollte nicht zu klein gesetzt werden, da manche Hoster den Zugriff sonst blockieren). IGNORE_ADDRS kann nochmals um einen eigenen Filter erweitert werden, an welche Adressen kein Autoreply gesendet werden soll. Falls Watchtower zur Aktualisierung von Containern genutzt wird, kann dies für den Autoreply-Container über das Label verhindert werden, da es ein custom build ist.

📋
docker-compose.yml
Copy to clipboard
services:
  autoreply:
    build: .
    container_name: autoreply
    restart: unless-stopped
    environment:
      IMAP_HOST: imap.host.de
      IMAP_USER: adresse@domain.de
      IMAP_PASS: "meinsupersicherespasswort"
      SMTP_HOST: smtp.host.de
      SMTP_PORT: 587
      SMTP_USER: adresse@domain.de
      SMTP_PASS: "meinsupersicherespasswort"
      POLL_SECONDS: 60
      REBOUND_HOURS: 24
      IGNORE_ADDRS: "no-reply@,noreply@,donotreply@"   # eigene Liste
    volumes:
      - /docker/autoreply/logs:/app/logs
    command: ["python", "-u", "autoreply.py"]
    labels:
      - "com.centurylinklabs.watchtower.enable=false"

reply.html

Der Inhalt der reply.html kann frei angepasst werden. Am Ende der Nachricht wird das Logo inline eingefügt. Die Größe des Logos kann ebenfalls über die style-Angabe angepasst werden. Soll kein Logo verwendet werden, kann einfach der Paragraph-Block gelöscht werden.

🌐
reply.html
Copy to clipboard
<html>
  <body>
    <p>
      Sehr geehrte Damen und Herren,<br><br>
      vielen Dank für Ihre Nachricht. Wir werden diese schnellstmöglich bearbeiten und uns mit Ihnen in Verbindung setzen.<br><br>
      Mit freundlichen Grüßen
    </p>

    <p>
      Das Team von PeetzCOM IT-Services 
    </p>
    <p>
      <img src="data:image/png;base64,{{INLINE_LOGO}}" alt="Logo" style="width:400px; height:auto;">
    </p>
  </body>
</html>

Erzeugen und starten des Containers

				
					docker compose up -d --build --no-cache
				
			
Nach oben scrollen