package report import ( "bytes" "context" "fmt" "html" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "adguard-shield/internal/config" "adguard-shield/internal/db" ) type Store interface { ReportStats(since, until int64, limit int) (db.ReportStats, error) } const cronPath = "/etc/cron.d/adguard-shield-report" func Status(c *config.Config) string { cron := "nicht installiert" if _, err := os.Stat(cronPath); err == nil { cron = "installiert (" + cronPath + ")" } return fmt.Sprintf(`E-Mail Report Aktiv: %v Intervall: %s Zeit: %s Empfaenger: %s Absender: %s Format: %s Mail-Befehl: %s Cron: %s `, c.ReportEnabled, c.ReportInterval, c.ReportTime, c.ReportEmailTo, c.ReportEmailFrom, c.ReportFormat, c.ReportMailCmd, cron) } func Generate(c *config.Config, st Store, format string) (string, error) { if format == "" { format = c.ReportFormat } since, until := window(c.ReportInterval) stats, err := st.ReportStats(since, until, 20) if err != nil { return "", err } if strings.EqualFold(format, "html") { return renderHTML(c, stats), nil } return renderText(c, stats), nil } func Send(ctx context.Context, c *config.Config, st Store) error { body, err := Generate(c, st, c.ReportFormat) if err != nil { return err } return sendMail(ctx, c, "AdGuard Shield Report", body) } func SendTest(ctx context.Context, c *config.Config) error { body := fmt.Sprintf("AdGuard Shield Test-Mail\n\nHostname: %s\nZeitpunkt: %s\nEmpfaenger: %s\nAbsender: %s\n", hostname(), time.Now().Format("2006-01-02 15:04:05"), c.ReportEmailTo, c.ReportEmailFrom) if strings.EqualFold(c.ReportFormat, "html") { body = "

AdGuard Shield Test-Mail

Hostname: " + html.EscapeString(hostname()) + "

Zeitpunkt: " + html.EscapeString(time.Now().Format("2006-01-02 15:04:05")) + "

" } return sendMail(ctx, c, "AdGuard Shield Test-Mail", body) } func InstallCron(binary, configPath string, c *config.Config) error { minute, hour, err := parseReportTime(c.ReportTime) if err != nil { return err } schedule := cronSchedule(c.ReportInterval, minute, hour) if binary == "" { binary = "/opt/adguard-shield/adguard-shield" } if configPath == "" { configPath = "/opt/adguard-shield/adguard-shield.conf" } line := fmt.Sprintf("SHELL=/bin/sh\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n%s root %s -config %s report-send\n", schedule, binary, configPath) return os.WriteFile(cronPath, []byte(line), 0644) } func RemoveCron() error { if err := os.Remove(cronPath); err != nil && !os.IsNotExist(err) { return err } return nil } func sendMail(ctx context.Context, c *config.Config, subject, body string) error { if c.ReportEmailTo == "" { return fmt.Errorf("REPORT_EMAIL_TO ist leer") } if c.ReportMailCmd == "" { return fmt.Errorf("REPORT_MAIL_CMD ist leer") } contentType := "text/plain; charset=utf-8" if strings.EqualFold(c.ReportFormat, "html") { contentType = "text/html; charset=utf-8" } msg := "From: " + c.ReportEmailFrom + "\n" + "To: " + c.ReportEmailTo + "\n" + "Subject: " + subject + "\n" + "Content-Type: " + contentType + "\n\n" + body parts := strings.Fields(c.ReportMailCmd) if len(parts) == 0 { return fmt.Errorf("REPORT_MAIL_CMD ist leer") } args := append(parts[1:], "-t") cmd := exec.CommandContext(ctx, parts[0], args...) cmd.Stdin = strings.NewReader(msg) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func parseReportTime(value string) (string, string, error) { parts := strings.Split(value, ":") if len(parts) != 2 { return "", "", fmt.Errorf("REPORT_TIME muss HH:MM sein") } hour, err := strconv.Atoi(parts[0]) if err != nil || hour < 0 || hour > 23 { return "", "", fmt.Errorf("REPORT_TIME hat ungueltige Stunde") } minute, err := strconv.Atoi(parts[1]) if err != nil || minute < 0 || minute > 59 { return "", "", fmt.Errorf("REPORT_TIME hat ungueltige Minute") } return strconv.Itoa(minute), strconv.Itoa(hour), nil } func cronSchedule(interval, minute, hour string) string { switch strings.ToLower(interval) { case "daily": return fmt.Sprintf("%s %s * * *", minute, hour) case "biweekly": return fmt.Sprintf("%s %s 1,15 * *", minute, hour) case "monthly": return fmt.Sprintf("%s %s 1 * *", minute, hour) default: return fmt.Sprintf("%s %s * * 1", minute, hour) } } func window(interval string) (int64, int64) { now := time.Now() days := 7 switch strings.ToLower(interval) { case "daily": days = 1 case "biweekly": days = 14 case "monthly": days = 30 } return now.AddDate(0, 0, -days).Unix(), now.Unix() } func renderText(c *config.Config, st db.ReportStats) string { var b strings.Builder b.WriteString("AdGuard Shield Report\n") b.WriteString("Zeitraum: " + formatTime(st.Since) + " bis " + formatTime(st.Until) + "\n\n") b.WriteString("Bans: " + strconv.Itoa(st.TotalBans) + "\n") b.WriteString("Unbans: " + strconv.Itoa(st.TotalUnbans) + "\n") b.WriteString("Aktive Sperren: " + strconv.Itoa(st.ActiveBans) + "\n\n") writeCountsText(&b, "Top Clients", st.TopClients) writeCountsText(&b, "Gruende", st.Reasons) writeCountsText(&b, "Aktive Quellen", st.Sources) if len(st.RecentEvents) > 0 { b.WriteString("Letzte Ereignisse\n") for _, e := range st.RecentEvents { b.WriteString("- " + e + "\n") } } _ = c return b.String() } func renderHTML(c *config.Config, st db.ReportStats) string { var b bytes.Buffer b.WriteString("AdGuard Shield Report") b.WriteString("") b.WriteString("") b.WriteString("

AdGuard Shield Report

") b.WriteString("

Zeitraum: " + html.EscapeString(formatTime(st.Since)) + " bis " + html.EscapeString(formatTime(st.Until)) + "

") b.WriteString("") writeCountsHTML(&b, "Top Clients", st.TopClients) writeCountsHTML(&b, "Gruende", st.Reasons) writeCountsHTML(&b, "Aktive Quellen", st.Sources) if len(st.RecentEvents) > 0 { b.WriteString("

Letzte Ereignisse

") for _, e := range st.RecentEvents { b.WriteString("") } b.WriteString("
Ereignis
" + html.EscapeString(e) + "
") } b.WriteString("") _ = c return b.String() } func writeCountsText(b *strings.Builder, title string, rows []db.ReportCount) { b.WriteString(title + "\n") if len(rows) == 0 { b.WriteString("- keine Daten\n\n") return } for _, r := range rows { b.WriteString("- " + r.Name + ": " + strconv.Itoa(r.Count) + "\n") } b.WriteByte('\n') } func writeCountsHTML(b *bytes.Buffer, title string, rows []db.ReportCount) { b.WriteString("

" + html.EscapeString(title) + "

") if len(rows) == 0 { b.WriteString("") } for _, r := range rows { b.WriteString("") } b.WriteString("
NameAnzahl
keine Daten
" + html.EscapeString(r.Name) + "" + strconv.Itoa(r.Count) + "
") } func formatTime(epoch int64) string { return time.Unix(epoch, 0).Format("2006-01-02 15:04:05") } func hostname() string { name, err := os.Hostname() if err != nil || name == "" { return filepath.Base(os.Args[0]) } return name }