first commit
This commit is contained in:
commit
fef0719277
8 changed files with 360 additions and 0 deletions
8
.env.example
Normal file
8
.env.example
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
96
README.md
Normal 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
14
docker-compose.yml
Normal 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
14
go.mod
Normal 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
12
go.sum
Normal 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
202
main.go
Normal 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")
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue