feat(qsl): credit line at the foot of the default QSL e-mail

"Designed & sent by OpsLog — https://github.com/GregTroar/OpsLog/releases/latest"
now closes the default body, so a fresh install sends it without being asked.

Put in the TEMPLATE, not appended at send time. Two consequences, both of them
the point: an operator who does not want it deletes the line once and it is
gone, and one who has already written their own body never sees it appear —
a stored template is returned verbatim and the default is only the fallback.
Appending it at send would have made it unremovable, which is not a thing to do
to somebody's outgoing mail.

The link is the human release page. updateCheckURL sits next to it in update.go
and answers JSON, which is the easy mistake here — a correspondent who clicks
the credit gets a download page, and a test says so.
This commit is contained in:
2026-08-16 11:07:06 +02:00
parent 46d6531cf5
commit c0dc1bfcc4
4 changed files with 62 additions and 5 deletions
+12 -1
View File
@@ -45,9 +45,20 @@ const appQSLCardSentField = "APP_OPSLOG_QSL_SENT"
// (please send one). Independent of ADIF qsl_rcvd, like the sent field.
const appQSLCardRcvdField = "APP_OPSLOG_QSL_RCVD"
// qslCredit closes the default QSL e-mail: who made the card, and where the
// recipient can get the same program.
//
// A DEFAULT, not a signature. It lives in the body template like every other
// line, so an operator who does not want it deletes it once and it is gone —
// and one who has already written their own body never sees it appear, because
// a stored template is returned verbatim and the default is only the fallback.
// Appending it at send time instead would have made it unremovable, which is
// not a thing to do to someone's outgoing mail.
const qslCredit = "--\nDesigned & sent by OpsLog — " + releasesPageURL
const (
defaultQSLEmailSubject = "eQSL — {CALL} de {MYCALL}"
defaultQSLEmailBody = "Hi,\n\nThank you for our QSO! Please find attached your eQSL card.\n\n{DATE} · {BAND} · {MODE}\n\n73,\n{MYCALL}"
defaultQSLEmailBody = "Hi,\n\nThank you for our QSO! Please find attached your eQSL card.\n\n{DATE} · {BAND} · {MODE}\n\n73,\n{MYCALL}\n\n" + qslCredit
)
// qslDir is the root of all designer artifacts: templates/<id>/ and outbox/.
+4 -2
View File
@@ -4,11 +4,13 @@
"date": "",
"en": [
"Right-click: update the US county of the selected contacts from the ULS database, replacing a county since renamed or abolished.",
"A QSO logged from WSJT-X, MSHV or a net now appears in Recent QSOs at once, instead of waiting for a delayed auto-upload to send it."
"A QSO logged from WSJT-X, MSHV or a net now appears in Recent QSOs at once, instead of waiting for a delayed auto-upload to send it.",
"New installs: the default QSL e-mail ends with a credit line and a link to OpsLog. It is part of the template, so delete it if you'd rather not."
],
"fr": [
"Clic droit : mettre à jour le comté US des contacts sélectionnés depuis la base ULS, pour remplacer un comté renommé ou supprimé.",
"Un QSO logué depuis WSJT-X, MSHV ou un net apparaît aussitôt dans les QSO récents, sans attendre lenvoi dun upload automatique différé."
"Un QSO logué depuis WSJT-X, MSHV ou un net apparaît aussitôt dans les QSO récents, sans attendre lenvoi dun upload automatique différé.",
"Nouvelles installations : le mail QSL par défaut se termine par une ligne de crédit et un lien vers OpsLog. Elle fait partie du modèle, supprimable."
]
},
{
+39
View File
@@ -0,0 +1,39 @@
package main
import (
"strings"
"testing"
)
// The default QSL e-mail closes with a credit line carrying the download page.
//
// It is part of the BODY TEMPLATE, deliberately: an operator who does not want
// it deletes it once, and one who already wrote their own body never sees it,
// because a stored template is returned verbatim and the default is only the
// fallback. Appending it at send time would have made it unremovable.
func TestQSLCreditIsInTheDefaultBody(t *testing.T) {
if !strings.HasSuffix(defaultQSLEmailBody, qslCredit) {
t.Errorf("the default QSL e-mail does not end with the credit line:\n%q", defaultQSLEmailBody)
}
// The body still has to be a usable template — the credit is added to it,
// not in place of it.
for _, v := range []string{"{DATE}", "{BAND}", "{MODE}", "{MYCALL}"} {
if !strings.Contains(defaultQSLEmailBody, v) {
t.Errorf("the default QSL e-mail lost %s", v)
}
}
}
// The link must be the page a person can read. updateCheckURL sits beside it and
// answers JSON — sending a correspondent there is the easy mistake to make.
func TestQSLCreditLinksTheHumanPage(t *testing.T) {
if !strings.Contains(qslCredit, releasesPageURL) {
t.Errorf("credit %q does not carry the releases page", qslCredit)
}
if strings.Contains(qslCredit, "api.github.com") {
t.Errorf("credit %q points at the update API, which answers JSON to whoever clicks it", qslCredit)
}
if !strings.Contains(qslCredit, "OpsLog") {
t.Errorf("credit %q does not name OpsLog", qslCredit)
}
}
+5
View File
@@ -23,6 +23,11 @@ import (
// build (the exe lives there; source stays on Gitea). Adjust the repo if needed.
const updateCheckURL = "https://api.github.com/repos/GregTroar/OpsLog/releases/latest"
// releasesPageURL is the same release, for people rather than for the updater:
// the API address above answers JSON, so it is not something to put in front of
// an operator who followed a link out of a QSL e-mail.
const releasesPageURL = "https://github.com/GregTroar/OpsLog/releases/latest"
// UpdateInfo is the result of the version check.
type UpdateInfo struct {
Current string `json:"current"` // this build's version (appVersion)