first commit

This commit is contained in:
Simon Rieger 2025-06-23 00:48:54 +02:00
commit fef0719277
8 changed files with 360 additions and 0 deletions

8
.env.example Normal file
View file

@ -0,0 +1,8 @@
IMAP_SERVER:
IMAP_PORT:
EMAIL_USER:
EMAIL_PASSWORD:
NTFY_SERVER:
NTFY_TOPIC:
NTFY_USER:
NTFY_PASSWORD:

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o imap-ntfy .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/imap-ntfy .
CMD ["./imap-ntfy"]

96
README.md Normal file
View file

@ -0,0 +1,96 @@
# IMAP-ntfy Notification Service
Eine Go-Anwendung, die jede Minute ein IMAP-Postfach abfragt und bei neuen E-Mails eine Benachrichtigung per ntfy sendet. Unterstützt variable ntfy-Server, Authentifizierung und einen Action-Button für Antworten.
## Features
- **Polling:** Überprüft alle 60 Sekunden das IMAP-Postfach auf neue E-Mails.
- **Benachrichtigung:** Sendet bei neuen E-Mails einen Push an einen ntfy-Server.
- **Authentifizierung:** Unterstützt Basic Auth für ntfy.
- **Action-Button:** Fügt einen "Antworten"-Button mit Reply-To-Adresse hinzu.
- **Konfiguration:** Alle Einstellungen erfolgen über Umgebungsvariablen.
- **Docker:** Einfache Deployment-Option mit Docker und Docker Compose.
- **Text-Inhalt:** Extrahiert den reinen Text-Inhalt der E-Mail und kürzt ihn auf 200 Zeichen.
## Voraussetzungen
- **Go** (mind. 1.21, nur für lokale Entwicklung)
- **Docker** und **Docker Compose**
- **IMAP-fähiges E-Mail-Postfach**
- **ntfy-Server** (z.B. `https://ntfy.sh` oder selbst gehostet)
## Konfiguration
Die Anwendung wird ausschließlich über Umgebungsvariablen konfiguriert:
| Variable | Beschreibung | Beispielwert |
|------------------|--------------------------------------|-----------------------------|
| `IMAP_SERVER` | IMAP-Server-Hostname | `imap.gmail.com` |
| `IMAP_PORT` | IMAP-Port | `993` |
| `EMAIL_USER` | E-Mail-Adresse | `your@email.com` |
| `EMAIL_PASSWORD` | E-Mail-Passwort oder App-Passwort | `your-secret-password` |
| `NTFY_SERVER` | ntfy-Server-URL | `https://ntfy.sh` |
| `NTFY_TOPIC` | ntfy-Topic | `my-github-notifications` |
| `NTFY_USER` | ntfy-Benutzername (optional) | `ntfy-username` |
| `NTFY_PASSWORD` | ntfy-Passwort (optional) | `ntfy-password` |
## Installation und Start
### 1. Repository klonen
```bash
git clone https://github.com/your-repository/imap-ntfy.git
cd imap-ntfy
```
### 2. Umgebungsvariablen anpassen
Passe die Werte in der Datei `docker-compose.yml` an deine Einstellungen an.
### 3. Docker Compose starten
```bash
docker-compose up --build -d
```
### 4. Logs überwachen
```bash
docker-compose logs -f
```
## Sicherheitshinweise
- **Verwende App-Passwörter** für E-Mail-Konten.
- **Sensible Daten** wie Passwörter sollten nicht in Klartext in Versionskontrollsystemen gespeichert werden.
- **Für ntfy:** Setze `NTFY_USER` und `NTFY_PASSWORD` für geschützte Topics.
## Beispiel für eine ntfy-Benachrichtigung
**Titel:**
`Testnachricht`
**Inhalt:**
```
Betreff: Testnachricht
Von: absender@example.com
Antwort an: absender@example.com
Hallo, dies ist eine Testnachricht. Hier steht der Inhalt der E-Mail, auf maximal 200 Zeichen gekürzt...
```
**Action-Button:**
"Antworten" öffnet das Standard-E-Mail-Programm mit vorausgefülltem Empfänger.
## Technische Details
- **Polling-Intervall:** 60 Sekunden
- **Markierung:** Nach erfolgreicher Benachrichtigung wird die E-Mail als gelesen markiert (`\Seen`)
- **Action-Button:** Fügt einen "Antworten"-Button mit Reply-To-Adresse hinzu
- **Text-Inhalt:** Es wird nur der reine Text-Inhalt der E-Mail extrahiert und verwendet
- **Flexibilität:** Unterstützt jeden ntfy-Server, auch selbst gehostet
---
**Hinweis:**
Die Anwendung ist für den produktiven Einsatz geeignet und kann leicht erweitert werden.

14
docker-compose.yml Normal file
View file

@ -0,0 +1,14 @@
services:
imap-ntfy:
build: .
environment:
IMAP_SERVER: ${IMAP_SERVER}
IMAP_PORT: ${IMAP_PORT}
EMAIL_USER: ${EMAIL_USER}
EMAIL_PASSWORD: ${EMAIL_PASSWORD}
NTFY_SERVER: ${NTFY_SERVER}
NTFY_TOPIC: ${NTFY_TOPIC}
NTFY_USER: ${NTFY_USER}
NTFY_PASSWORD: ${NTFY_PASSWORD}
restart: unless-stopped
networks: {}

14
go.mod Normal file
View file

@ -0,0 +1,14 @@
module imap-ntfy
go 1.24.1
require (
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-message v0.15.0
)
require (
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
golang.org/x/text v0.3.7 // indirect
)

12
go.sum Normal file
View file

@ -0,0 +1,12 @@
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

202
main.go Normal file
View file

@ -0,0 +1,202 @@
package main
import (
"crypto/tls"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
)
func main() {
log.SetOutput(os.Stdout)
log.Println("IMAP-ntfy Notification Service gestartet")
imapServer := os.Getenv("IMAP_SERVER")
imapPort := os.Getenv("IMAP_PORT")
emailUser := os.Getenv("EMAIL_USER")
emailPass := os.Getenv("EMAIL_PASSWORD")
ntfyServer := os.Getenv("NTFY_SERVER")
ntfyTopic := os.Getenv("NTFY_TOPIC")
ntfyUser := os.Getenv("NTFY_USER")
ntfyPass := os.Getenv("NTFY_PASSWORD")
if ntfyServer == "" {
ntfyServer = "https://ntfy.sh"
log.Println("NTFY_SERVER nicht gesetzt, verwende Standardwert:", ntfyServer)
}
for {
log.Println("Starte IMAP-Abfrage...")
addr := fmt.Sprintf("%s:%s", imapServer, imapPort)
c, err := client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true})
if err != nil {
log.Printf("IMAP-Verbindungsfehler: %v", err)
time.Sleep(1 * time.Minute)
continue
}
defer c.Logout()
if err := c.Login(emailUser, emailPass); err != nil {
log.Printf("IMAP-Login fehlgeschlagen: %v", err)
time.Sleep(1 * time.Minute)
continue
}
_, err = c.Select("INBOX", false)
if err != nil {
log.Printf("Postfachauswahl fehlgeschlagen: %v", err)
time.Sleep(1 * time.Minute)
continue
}
criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{"\\Seen"}
ids, err := c.Search(criteria)
if err != nil {
log.Printf("Suche fehlgeschlagen: %v", err)
time.Sleep(1 * time.Minute)
continue
}
if len(ids) > 0 {
log.Printf("Gefunden: %d neue Nachrichten", len(ids))
seqset := new(imap.SeqSet)
seqset.AddNum(ids...)
section := &imap.BodySectionName{}
items := []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()}
messages := make(chan *imap.Message, 10)
go func() {
if err := c.Fetch(seqset, items, messages); err != nil {
log.Printf("Fetch fehlgeschlagen: %v", err)
}
}()
for msg := range messages {
subject := msg.Envelope.Subject
from := ""
if len(msg.Envelope.From) > 0 {
from = msg.Envelope.From[0].Address()
}
replyTo := ""
if len(msg.Envelope.ReplyTo) > 0 {
replyTo = msg.Envelope.ReplyTo[0].Address()
} else if from != "" {
replyTo = from
}
var bodyText string
if r := msg.GetBody(section); r != nil {
bodyBytes, err := io.ReadAll(r)
if err != nil {
log.Printf("Fehler beim Lesen des Bodys: %v", err)
} else {
bodyText = extractTextFromBody(bodyBytes)
if len(bodyText) > 200 {
bodyText = bodyText[:200] + "..."
}
}
}
message := fmt.Sprintf(
"Betreff: %s\nVon: %s\nAntwort an: %s\n\n%s",
subject, from, replyTo, bodyText,
)
sendNtfyNotification(ntfyServer, ntfyTopic, ntfyUser, ntfyPass,
subject, message, replyTo)
}
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.SeenFlag}
if err := c.Store(seqset, item, flags, nil); err != nil {
log.Printf("Markieren fehlgeschlagen: %v", err)
}
} else {
log.Println("Keine neuen Nachrichten gefunden.")
}
c.Logout()
log.Println("Warte 1 Minute bis zur nächsten Abfrage...")
time.Sleep(1 * time.Minute)
}
}
func extractTextFromBody(bodyBytes []byte) string {
r := strings.NewReader(string(bodyBytes))
mr, err := mail.CreateReader(r)
if err != nil {
return string(bodyBytes)
}
var textContent strings.Builder
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
continue
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
contentType, _, _ := h.ContentType()
if contentType == "text/plain" {
partBytes, _ := io.ReadAll(p.Body)
textContent.Write(partBytes)
}
}
}
if textContent.Len() > 0 {
return textContent.String()
}
return string(bodyBytes)
}
func sendNtfyNotification(server, topic, user, pass, title, message, replyTo string) {
url := fmt.Sprintf("%s/%s", server, topic)
log.Printf("Sende ntfy-Benachrichtigung an %s", url)
req, _ := http.NewRequest("POST", url, strings.NewReader(message))
req.Header.Set("Title", title)
req.Header.Set("Content-Type", "text/plain")
if replyTo != "" {
action := fmt.Sprintf("view, Antworten, mailto:%s", replyTo)
req.Header.Set("Actions", action)
log.Printf("Füge Action-Button hinzu: %s", action)
}
if user != "" && pass != "" {
req.SetBasicAuth(user, pass)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("ntfy-Fehler: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("ntfy-Fehlerstatus: %d", resp.StatusCode)
log.Printf("Fehlermeldung: %s", string(body))
} else {
log.Println("ntfy-Benachrichtigung erfolgreich gesendet")
}
}