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
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
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“.
#!/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 Zeitstempelpython manage_db.py clear
→ leert die komplette Tabellepython 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
#!/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.
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.
<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