- Station Control: the amplifier panel is now the exact same card as the FlexRadio panel (extracted shared AmpCard: OPERATE/STANDBY, ON/OFF, power level, output-power bar, live FWD/ID/temp meters). Every configured amp gets its own card — no dropdown. - Help → "Send log to F4BPO" (only when SMTP is configured): e-mails opslog.log + previous session + crash log to the developer. New email.SendFiles for multi-attachment. - Bulk edit: new "Frequency (MHz)" field sets freq_hz AND recomputes band; out-of-band values rejected. Fixes a run logged on a stale/default freq after CAT dropped. - Cluster: Time column sorted lexically on the HHMMZ string, so spots after 0000Z sorted below 2359Z and looked frozen at the UTC rollover. Now sorts by real arrival time. - Also folds in the DVK auto-CQ/2-column layout and Station Control dashboard batch (changelog 0.20.10, EN+FR).
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// Package email sends QSO recordings to correspondents via SMTP. Pure Go (no
|
|
// CGO) using go-mail; supports implicit SSL (465), STARTTLS (587) or none.
|
|
package email
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/wneessen/go-mail"
|
|
)
|
|
|
|
// Config is the user's SMTP configuration.
|
|
type Config struct {
|
|
Host string
|
|
Port int
|
|
User string
|
|
Password string
|
|
From string
|
|
ReplyTo string // optional Reply-To: where replies should go (e.g. a personal inbox)
|
|
Encryption string // "ssl" | "starttls" | "none"
|
|
Auth bool // SMTP requires authorization (send username/password)
|
|
}
|
|
|
|
func (c Config) opts() []mail.Option {
|
|
o := []mail.Option{mail.WithPort(c.Port), mail.WithTimeout(30 * time.Second)}
|
|
if c.Auth && c.User != "" {
|
|
// AutoDiscover negotiates whatever mechanism the server advertises
|
|
// (LOGIN, PLAIN, CRAM-MD5, …). OVH, for instance, rejects forced PLAIN.
|
|
o = append(o, mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithUsername(c.User), mail.WithPassword(c.Password))
|
|
}
|
|
switch c.Encryption {
|
|
case "ssl":
|
|
o = append(o, mail.WithSSL())
|
|
case "none":
|
|
o = append(o, mail.WithTLSPolicy(mail.NoTLS))
|
|
default: // starttls
|
|
o = append(o, mail.WithTLSPolicy(mail.TLSMandatory))
|
|
}
|
|
return o
|
|
}
|
|
|
|
// Send delivers a plain-text email to `to`, optionally attaching a file.
|
|
func Send(cfg Config, to, subject, body, attachPath string) error {
|
|
var attach []string
|
|
if attachPath != "" {
|
|
attach = []string{attachPath}
|
|
}
|
|
return SendFiles(cfg, to, subject, body, attach)
|
|
}
|
|
|
|
// SendFiles is like Send but attaches any number of files (missing paths are
|
|
// skipped by the mail library at send time).
|
|
func SendFiles(cfg Config, to, subject, body string, attachPaths []string) error {
|
|
if cfg.Host == "" {
|
|
return fmt.Errorf("SMTP server not configured")
|
|
}
|
|
if to == "" {
|
|
return fmt.Errorf("no recipient e-mail")
|
|
}
|
|
from := cfg.From
|
|
if from == "" {
|
|
from = cfg.User
|
|
}
|
|
m := mail.NewMsg()
|
|
if err := m.From(from); err != nil {
|
|
return fmt.Errorf("bad sender %q: %w", from, err)
|
|
}
|
|
if err := m.To(to); err != nil {
|
|
return fmt.Errorf("bad recipient %q: %w", to, err)
|
|
}
|
|
if cfg.ReplyTo != "" {
|
|
if err := m.ReplyTo(cfg.ReplyTo); err != nil {
|
|
return fmt.Errorf("bad reply-to %q: %w", cfg.ReplyTo, err)
|
|
}
|
|
}
|
|
m.Subject(subject)
|
|
m.SetBodyString(mail.TypeTextPlain, body)
|
|
for _, p := range attachPaths {
|
|
if p != "" {
|
|
m.AttachFile(p)
|
|
}
|
|
}
|
|
client, err := mail.NewClient(cfg.Host, cfg.opts()...)
|
|
if err != nil {
|
|
return fmt.Errorf("smtp client: %w", err)
|
|
}
|
|
if err := client.DialAndSend(m); err != nil {
|
|
return fmt.Errorf("send: %w", err)
|
|
}
|
|
return nil
|
|
}
|