aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.adoc19
-rw-r--r--README.md38
-rw-r--r--go.mod1
-rw-r--r--go.sum4
-rw-r--r--lilin.go2
-rw-r--r--notif_config.go56
-rw-r--r--notif_config_test.go51
-rw-r--r--server_config_test.go27
-rw-r--r--testdata/server_config/ok/etc/lilin/lilin.cfg14
-rw-r--r--testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg2
-rw-r--r--worker.go86
12 files changed, 270 insertions, 32 deletions
diff --git a/.gitignore b/.gitignore
index d1dc80f..fcbd285 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
## SPDX-License-Identifier: GPL-3.0-only
## SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
-/_www/doc/index.html
+/_www/doc/*.html
/cover.html
/cover.out
/lilin
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index c1e45aa..38d3154 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -1,13 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
-= lilin releases changelog
+= lilin release changelog
:sectanchors:
:toc:
-Change log of each release of the lilin software.
-The latest release log is put on the top.
-
Legend,
* 🪵: Breaking changes
@@ -15,6 +12,20 @@ Legend,
* 🌼: Enhancement
* 💧: Chores
+
+[#lilin_v0_2_0]
+== lilin v0.2.0 (2026-xx-xx)
+
+**🪵 all: rename notif field "webhook_url" to "remote_url"
+
+These changes is to minimize duplicate field on "notif" section, so
+different kind of notif can use the same "remote_url" value.
+
+**🌱 all: support sending notification to SMTP server (email)**
+
+In the main configuration, lilin.cfg, user now can add "notif" section with
+kind "smtp" to send notification using user's email.
+
[#lilin_v0_1_0]
== lilin v0.1.0 (2025-12-27)
diff --git a/README.md b/README.md
index 083932d..3fbc094 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,7 @@ timeout = 60s
```
Sample of service configuration that monitor TCP service at
-127.0.0.1:5432 every 90 seconds with timeout 30 seconds,
+127.0.0.1:5432 every 90 seconds with 30 seconds timeout,
```
[service]
@@ -127,18 +127,21 @@ timeout = 30s
Lilin support sending notification to,
- Mattermost using incoming webhook.
+- SMTP server (email).
See the next section on how to use the notification.
### Mattermost incoming webhook
+<!--{{{-->
+
In the main configuration, add the section "notif" with the following
format,
```
[notif]
kind = mattermost
-webhook_url = # The incoming webhook URL.
+remote_url = # The incoming webhook URL.
channel = # The channel where the notification will be placed.
down_template = # Message template when service is down.
up_template = # Message template when service is up.
@@ -170,6 +173,37 @@ will be rendered as
2025-09-26 06:38:11 +0000 UTC: Service http-server is down: 503 Service Unavailable
```
+<!--}}}-->
+
+### SMTP server
+
+<!--{{{-->
+
+```
+[notif]
+kind = smtp
+remote_url = <scheme "://" (domain | ip_address [":" port])>
+user =
+password =
+recipient =
+```
+
+The `remote_url` define the SMTP server address.
+The scheme in `remote_url` can be set either to,
+
+- smtps: implicit TLS, connect to port 465 by default
+- smtp+starttls: explicit STARTTLS, connect to port 587 by default.
+
+The `user` field define the sender email address.
+
+The `password` field define the password for authentication, on behalf of
+the `user`.
+
+The `recipient` is the email address that will receives the notification.
+This field can be defined multiple times.
+
+<!--}}}-->
+
## Links
[Project page](https://sr.ht/~shulhan/lilin).
diff --git a/go.mod b/go.mod
index 068deae..a19892a 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ go 1.24.0
require git.sr.ht/~shulhan/pakakeh.go v0.60.2
require (
+ golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
diff --git a/go.sum b/go.sum
index c6e6ef6..6f5f55b 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ git.sr.ht/~shulhan/pakakeh.go v0.60.2 h1:ZSRE77lYm+mkhvg9pSrxCIO81ydbqt93qbsWuZJ
git.sr.ht/~shulhan/pakakeh.go v0.60.2/go.mod h1:1MkKXbLZRHTcnheeSEbRpGztkym4Yxzh90ep+jCxbDc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
@@ -12,5 +14,7 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
diff --git a/lilin.go b/lilin.go
index acf8e21..b855692 100644
--- a/lilin.go
+++ b/lilin.go
@@ -9,7 +9,7 @@ import (
)
// Version latest released version.
-const Version = `0.1.0`
+const Version = `0.2.0`
// defTimeout define default timeout for service and client connection.
const defTimeout = 5 * time.Second
diff --git a/notif_config.go b/notif_config.go
index 7fa7d2d..c3ed43c 100644
--- a/notif_config.go
+++ b/notif_config.go
@@ -11,43 +11,75 @@ import (
const (
notifKindMattermost = `mattermost`
+ notifKindSMTP = `smtp`
)
type NotifConfig struct {
- webhookURL *url.URL
- downTmpl *template.Template
- upTmpl *template.Template
+ remoteURL *url.URL
+
+ downTmpl *template.Template
+ upTmpl *template.Template
+
+ Kind string `ini:"notif::kind"`
+
+ // RemoteURL define the remote server to send notification.
+ // For mattermost this is the webhook URL.
+ // For smtp this is the domain or IP of SMTP server.
+ RemoteURL string `ini:"notif::remote_url"`
+
+ // Channel that receive the mattermost message.
+ Channel string `ini:"notif::channel"`
+
+ // User for authentication with SMTP server.
+ User string `ini:"notif::user"`
+
+ // Password for authentication with SMTP server.
+ Password string `ini:"notif::password"`
- Kind string `ini:"notif::kind"`
- WebhookURL string `ini:"notif::webhook_url"`
- Channel string `ini:"notif::channel"`
DownTemplate string `ini:"notif::down_template"`
UpTemplate string `ini:"notif::up_template"`
+
+ // Recipient of email.
+ Recipient []string `ini:"notif::recipient"`
}
func (notifConfig *NotifConfig) init() (err error) {
+ var logp = `notifConfig.init`
+
switch notifConfig.Kind {
case notifKindMattermost:
- notifConfig.webhookURL, err = url.Parse(notifConfig.WebhookURL)
+ notifConfig.remoteURL, err = url.Parse(notifConfig.RemoteURL)
if err != nil {
- return fmt.Errorf(`invalid WebhookURL %q: %s`,
- notifConfig.WebhookURL, err)
+ return fmt.Errorf(`invalid RemoteURL %q: %s`,
+ notifConfig.RemoteURL, err)
+ }
+ case notifKindSMTP:
+ if notifConfig.User == `` {
+ return fmt.Errorf(`%s: %s: empty user`, logp, notifConfig.Kind)
+ }
+ if notifConfig.Password == `` {
+ return fmt.Errorf(`%s: %s: empty password`, logp, notifConfig.Kind)
+ }
+ if len(notifConfig.Recipient) == 0 {
+ return fmt.Errorf(`%s: %s: empty recipient`, logp, notifConfig.Kind)
}
default:
- return fmt.Errorf(`unknown notif kind: %s`, notifConfig.Kind)
+ return fmt.Errorf(`%s: unknown notif kind %q`, logp, notifConfig.Kind)
}
if notifConfig.DownTemplate != "" {
notifConfig.downTmpl = template.New(`down`)
notifConfig.downTmpl, err = notifConfig.downTmpl.Parse(notifConfig.DownTemplate)
if err != nil {
- return fmt.Errorf(`failed to parse down_template: %s`, err)
+ return fmt.Errorf(`%s: %s: failed to parse down_template: %s`,
+ logp, notifConfig.Kind, err)
}
}
if notifConfig.UpTemplate != "" {
notifConfig.upTmpl = template.New(`up`)
notifConfig.upTmpl, err = notifConfig.upTmpl.Parse(notifConfig.UpTemplate)
if err != nil {
- return fmt.Errorf(`failed to parse up_template: %s`, err)
+ return fmt.Errorf(`%s: %s: failed to parse up_template: %s`,
+ logp, notifConfig.Kind, err)
}
}
return nil
diff --git a/notif_config_test.go b/notif_config_test.go
new file mode 100644
index 0000000..b7990b1
--- /dev/null
+++ b/notif_config_test.go
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
+
+package lilin
+
+import (
+ "testing"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+)
+
+func TestNotifConfig_init(t *testing.T) {
+ listCase := []struct {
+ expError string
+ config NotifConfig
+ }{{
+ config: NotifConfig{
+ Kind: notifKindSMTP,
+ },
+ expError: `notifConfig.init: smtp: empty user`,
+ }, {
+ config: NotifConfig{
+ Kind: notifKindSMTP,
+ User: `john.doe@example.local`,
+ },
+ expError: `notifConfig.init: smtp: empty password`,
+ }, {
+ config: NotifConfig{
+ Kind: notifKindSMTP,
+ User: `john.doe@example.local`,
+ Password: `dummy`,
+ },
+ expError: `notifConfig.init: smtp: empty recipient`,
+ }, {
+ config: NotifConfig{
+ Kind: notifKindSMTP,
+ User: `john.doe@example.local`,
+ Password: `dummy`,
+ Recipient: []string{
+ `jane.doe@example.local`,
+ },
+ },
+ }}
+ for _, tc := range listCase {
+ err := tc.config.init()
+ if err != nil {
+ test.Assert(t, ``, tc.expError, err.Error())
+ continue
+ }
+ }
+}
diff --git a/server_config_test.go b/server_config_test.go
index bb30e5c..bd17b5e 100644
--- a/server_config_test.go
+++ b/server_config_test.go
@@ -16,15 +16,15 @@ func TestServerConfig_init(t *testing.T) {
exp ServerConfig
}
- var webhookURL12000 *url.URL
- var webhookURL12001 *url.URL
+ var remoteURL12000 *url.URL
+ var remoteURL12001 *url.URL
var err error
- webhookURL12000, err = url.Parse(`http://127.0.0.1:12000`)
+ remoteURL12000, err = url.Parse(`http://127.0.0.1:12000`)
if err != nil {
t.Fatal(err)
}
- webhookURL12001, err = url.Parse(`http://127.0.0.1:12001`)
+ remoteURL12001, err = url.Parse(`http://127.0.0.1:12001`)
if err != nil {
t.Fatal(err)
}
@@ -39,18 +39,29 @@ func TestServerConfig_init(t *testing.T) {
Address: `127.0.0.1:12121`,
Notifs: []*NotifConfig{{
Kind: `mattermost`,
- WebhookURL: `http://127.0.0.1:12000`,
+ RemoteURL: `http://127.0.0.1:12000`,
Channel: `chan_1`,
UpTemplate: `Service {{.ID}} is up again!`,
DownTemplate: `Service {{.ID}} is down: {{.Error}}`,
- webhookURL: webhookURL12000,
+ remoteURL: remoteURL12000,
}, {
Kind: `mattermost`,
- WebhookURL: `http://127.0.0.1:12001`,
+ RemoteURL: `http://127.0.0.1:12001`,
Channel: `chan_2`,
UpTemplate: `Service {{.ID}} is alive!`,
DownTemplate: `Service {{.ID}} is down: {{.Error}}`,
- webhookURL: webhookURL12001,
+ remoteURL: remoteURL12001,
+ }, {
+ Kind: `smtp`,
+ RemoteURL: `smtps://127.0.0.1:4650`,
+ User: `john.doe@example.local`,
+ Password: `dummy`,
+ Recipient: []string{
+ `jane@example.local`,
+ `kent@example.local`,
+ },
+ UpTemplate: `Service {{.ID}} is alive!`,
+ DownTemplate: `Service {{.ID}} is down: {{.Error}}`,
}},
},
}}
diff --git a/testdata/server_config/ok/etc/lilin/lilin.cfg b/testdata/server_config/ok/etc/lilin/lilin.cfg
index ef179d4..901d9e6 100644
--- a/testdata/server_config/ok/etc/lilin/lilin.cfg
+++ b/testdata/server_config/ok/etc/lilin/lilin.cfg
@@ -6,14 +6,24 @@ address = 127.0.0.1:12121
[notif]
kind = mattermost
-webhook_url = http://127.0.0.1:12000
+remote_url = http://127.0.0.1:12000
channel = chan_1
down_template = Service {{.ID}} is down: {{.Error}}
up_template = Service {{.ID}} is up again!
[notif]
kind = mattermost
-webhook_url = http://127.0.0.1:12001
+remote_url = http://127.0.0.1:12001
channel = chan_2
down_template = Service {{.ID}} is down: {{.Error}}
up_template = Service {{.ID}} is alive!
+
+[notif]
+kind = smtp
+remote_url = smtps://127.0.0.1:4650
+user = john.doe@example.local
+password = dummy
+recipient = jane@example.local
+recipient = kent@example.local
+down_template = Service {{.ID}} is down: {{.Error}}
+up_template = Service {{.ID}} is alive!
diff --git a/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg b/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg
index 3c479f3..2225609 100644
--- a/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg
+++ b/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg
@@ -6,7 +6,7 @@ address = 127.0.0.1:12121
[notif]
kind = mattermost
-webhook_url = http://127.0.0.1:6330/mattermost
+remote_url = http://127.0.0.1:6330/mattermost
channel = test_webhook
down_template = Service {{.ID}} is down: {{.Error}}
up_template = Service {{.ID}} is up again!
diff --git a/worker.go b/worker.go
index bccfe74..b39b047 100644
--- a/worker.go
+++ b/worker.go
@@ -6,6 +6,7 @@ package lilin
import (
"bytes"
"encoding/json"
+ "fmt"
"io"
"log"
"net/http"
@@ -14,7 +15,9 @@ import (
"strings"
"time"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/email"
"git.sr.ht/~shulhan/pakakeh.go/lib/ini"
+ "git.sr.ht/~shulhan/pakakeh.go/lib/smtp"
)
// worker contains the report of all services.
@@ -174,6 +177,8 @@ func (wrk *worker) handlePushNotif() {
switch notifConfig.Kind {
case notifKindMattermost:
wrk.pushNotifMattermost(notifConfig, &scanReport)
+ case notifKindSMTP:
+ wrk.pushNotifSMTP(notifConfig, &scanReport)
}
}
}
@@ -213,7 +218,7 @@ func (wrk *worker) pushNotifMattermost(
var req = &http.Request{
Method: http.MethodPost,
- URL: notifConfig.webhookURL,
+ URL: notifConfig.remoteURL,
Header: http.Header{
`Content-Type`: []string{
`application/json`,
@@ -242,3 +247,82 @@ func (wrk *worker) pushNotifMattermost(
log.Printf(`%s: fail with status code %d: %s`, logp, resp.StatusCode, body)
}
+
+// pushNotifSMTP send notification through SMTP.
+func (wrk *worker) pushNotifSMTP(notifConfig *NotifConfig, scanReport *ScanReport) {
+ var logp = `pushNotifSMTP`
+ var msg = email.Message{}
+ var err error
+
+ err = msg.SetFrom(notifConfig.User)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+
+ for _, recipient := range notifConfig.Recipient {
+ err = msg.AddTo(recipient)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+ }
+
+ var subject string
+ var text bytes.Buffer
+ if scanReport.Success {
+ subject = fmt.Sprintf(`[lilin] Service %s is up!`, scanReport.ID)
+ err = notifConfig.upTmpl.Execute(&text, scanReport)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+ } else {
+ subject = fmt.Sprintf(`[lilin] Service %s is down!`, scanReport.ID)
+ err = notifConfig.downTmpl.Execute(&text, scanReport)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+ }
+
+ msg.SetSubject(subject)
+
+ err = msg.SetBodyText(text.Bytes())
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+
+ var data []byte
+ data, err = msg.Pack()
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+
+ var mailtx = smtp.NewMailTx(notifConfig.User, notifConfig.Recipient, data)
+
+ var clientOpts = smtp.ClientOptions{
+ ServerURL: notifConfig.RemoteURL,
+ AuthUser: notifConfig.User,
+ AuthPass: notifConfig.Password,
+ AuthMechanism: smtp.SaslMechanismPlain,
+ }
+ var smtpc *smtp.Client
+ smtpc, err = smtp.NewClient(clientOpts)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ return
+ }
+
+ _, err = smtpc.MailTx(mailtx)
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ }
+
+ _, err = smtpc.Quit()
+ if err != nil {
+ log.Printf(`%s: %s`, logp, err)
+ }
+}