Skip to content

Commit f7f111d

Browse files
committed
feat: welcome erida
1 parent 6828a0e commit f7f111d

File tree

15 files changed

+867
-89
lines changed

15 files changed

+867
-89
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
.env
1+
*.env
22
.idea
33
.DS_Store

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ ifneq (,$(wildcard ./.env))
44
endif
55

66
run:
7-
@go run main.go
7+
@go run cmd/main.go
8+
9+
integration:
10+
@go test -v ./... -tags=integration
11+
12+

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
### Erida: Simplifying Internal Cluster Communication
2+
3+
**Erida** is a straightforward SMTP relay server designed for sending internal cluster emails to an authenticated SMTP
4+
server with seamless Slack integration.
5+
6+
### Email Address Flexibility
7+
8+
Erida supports a variety of email addresses, including common ones like `fadyat@icloud.com` and those associated with
9+
messaging services, particularly Slack.
10+
11+
For Slack integration, you can use addresses following this syntax:
12+
13+
- User-specific Slack address: `personal.<username>@slack`
14+
- Channel-specific Slack address: `channel.<channelname>@slack`
15+
16+
It's important to note that both the `username` and `channelname` are case-insensitive, and the bot must have the
17+
necessary
18+
permissions to access the specified Slack channels.
19+
20+
### Configuration Made Easy
21+
22+
Configuring Erida is a breeze. All you need to do is set the following environment variables:
23+
24+
- `SMTP_HOST`: SMTP server host
25+
- `SMTP_PORT`: SMTP server port
26+
- `SMTP_USER`: SMTP server username
27+
- `SMTP_PASSWORD`: SMTP server password
28+
- `SLACK_TOKEN`: Slack bot token
29+
- `SMTP_TLS` (Optional, default: true): Enable or disable Start TLS usage
30+
31+
For additional variables, refer to the [configuration file](internal/config.go).
32+
33+
### Getting Started
34+
35+
If you're new to configuring the bot, check out the step-by-step guide
36+
at [Slack Quickstart](https://api.slack.com/start/quickstart).
37+
38+
Ensure that the bot has the necessary permissions, specifically `chat:write`.
39+
40+
### Example Usage
41+
42+
Let's walk through an example. Assume that an external SMTP server is configured to send emails to **Erida** with the
43+
following addresses: `personal.fadyat@slack`, `channel.general@slack`, and `fadyat@icloud.com`.
44+
45+
The message will be seamlessly delivered to the Slack channel `#general` and the Slack user `@fadyat`, as well as to the
46+
email address `fadyat@icloud.com`.
47+
48+
**Erida** simplifies internal communication, bridging the gap between email and Slack effortlessly.
49+

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package main
22

33
import (
4+
"github.com/fadyat/erida/internal"
45
"github.com/ilyakaznacheev/cleanenv"
56
"log/slog"
6-
"smpt-relay/internal"
77
)
88

99
func readConfig() (*internal.Config, error) {

cmd/main_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//go:build integration
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"github.com/emersion/go-smtp"
9+
"github.com/fadyat/erida/internal"
10+
"github.com/ilyakaznacheev/cleanenv"
11+
"github.com/stretchr/testify/require"
12+
"log/slog"
13+
"testing"
14+
"time"
15+
)
16+
17+
const (
18+
testConfigPath = "../test.env"
19+
)
20+
21+
func readTestConfig() (*internal.Config, error) {
22+
var cfg internal.Config
23+
if err := cleanenv.ReadConfig(testConfigPath, &cfg); err != nil {
24+
return nil, err
25+
}
26+
27+
return &cfg, nil
28+
}
29+
30+
// TestRelayFlow is an integration test, that will start a server and send a
31+
// message to it.
32+
//
33+
// Need to run it with `go test -tags=integration ./...`
34+
//
35+
// Real Google GMail SMTP server and Slack API are used.
36+
// Current test doesn't check the correctness of the message, that was sent.
37+
// It only checks, that the server is able to send a message to the real services.
38+
func TestRelayFlow(t *testing.T) {
39+
cfg, err := readTestConfig()
40+
require.NoError(t, err)
41+
42+
srv := internal.NewServer(cfg)
43+
go func() {
44+
if err = srv.ListenAndServe(); err != nil {
45+
slog.Error("failed to start server: ", err)
46+
}
47+
}()
48+
49+
c, err := smtp.Dial(srv.Addr)
50+
require.NoError(t, err)
51+
52+
var (
53+
from = "erida@erida.com"
54+
to = []string{
55+
"fadyat@icloud.com",
56+
"personal.fadyat@slack",
57+
"channel.empty@slack",
58+
}
59+
body = []byte(`Subject: Integration test
60+
Content-Type: text/plain; charset=UTF-8
61+
62+
Hello, this Erida!
63+
We are testing the process of sending messages to Slack and GMail,
64+
using this SMTP server as a relay.
65+
66+
Best regards, Erida
67+
`)
68+
)
69+
70+
require.NoError(t, c.SendMail(from, to, bytes.NewReader(body)))
71+
72+
// sleeping for 3 seconds to let the server process the message
73+
time.Sleep(3 * time.Second)
74+
75+
require.NoError(t, c.Quit())
76+
require.NoError(t, srv.Shutdown(context.Background()))
77+
}

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module smpt-relay
1+
module github.com/fadyat/erida
22

33
go 1.21
44

@@ -7,14 +7,18 @@ require (
77
github.com/emersion/go-smtp v0.19.0
88
github.com/ilyakaznacheev/cleanenv v1.5.0
99
github.com/mocktools/go-smtp-mock/v2 v2.1.0
10+
github.com/slack-go/slack v0.12.3
1011
github.com/stretchr/testify v1.8.4
1112
)
1213

1314
require (
1415
github.com/BurntSushi/toml v1.3.2 // indirect
1516
github.com/davecgh/go-spew v1.1.1 // indirect
17+
github.com/gorilla/websocket v1.5.1 // indirect
1618
github.com/joho/godotenv v1.5.1 // indirect
1719
github.com/pmezard/go-difflib v1.0.0 // indirect
20+
github.com/stretchr/objx v0.5.0 // indirect
21+
golang.org/x/net v0.18.0 // indirect
1822
gopkg.in/yaml.v3 v3.0.1 // indirect
1923
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
2024
)

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1X
88
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
99
github.com/emersion/go-smtp v0.19.0 h1:iVCDtR2/JY3RpKoaZ7u6I/sb52S3EzfNHO1fAWVHgng=
1010
github.com/emersion/go-smtp v0.19.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
11+
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
12+
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
13+
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
14+
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
15+
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
16+
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
17+
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
1118
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
1219
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
1320
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -16,14 +23,20 @@ github.com/mocktools/go-smtp-mock/v2 v2.1.0 h1:gGiWqlaMTExk7Id38G2+sWfOelsE+OAqJ
1623
github.com/mocktools/go-smtp-mock/v2 v2.1.0/go.mod h1:n8aNpDYncZHH/cZHtJKzQyeYT/Dut00RghVM+J1Ed94=
1724
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1825
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26+
github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88=
27+
github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
1928
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
2029
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
2130
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
2231
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
32+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
2333
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2434
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
2535
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
2636
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
37+
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
38+
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
39+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
2740
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2841
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2942
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/client.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"github.com/slack-go/slack"
6+
"strings"
7+
)
8+
9+
const (
10+
emailRecipient = "email"
11+
slackRecipient = "slack"
12+
13+
personalMessage = "personal."
14+
channelMessage = "channel."
15+
)
16+
17+
var (
18+
operators = map[string]map[string]string{
19+
slackRecipient: {
20+
personalMessage: "@",
21+
channelMessage: "#",
22+
},
23+
}
24+
)
25+
26+
type client interface {
27+
28+
// proxify is a function, that will be used as a proxy for the data.
29+
proxify(from string, to []string, body string) error
30+
}
31+
32+
func takeUsernames(recipients []string, recipientType string) []string {
33+
var usernames = make([]string, 0, len(recipients))
34+
35+
for _, r := range recipients {
36+
at := strings.Split(r, "@")
37+
if len(at) != 2 {
38+
continue
39+
}
40+
41+
if at[1] != recipientType {
42+
continue
43+
}
44+
45+
usernames = append(usernames, convertToRecipientWay(at[0], recipientType))
46+
}
47+
48+
return usernames
49+
}
50+
51+
func convertToRecipientWay(username string, recipientType string) string {
52+
if _, ok := operators[recipientType]; !ok {
53+
return username
54+
}
55+
56+
var msgType string
57+
switch {
58+
case strings.HasPrefix(username, personalMessage):
59+
msgType = personalMessage
60+
case strings.HasPrefix(username, channelMessage):
61+
msgType = channelMessage
62+
}
63+
64+
operator := operators[recipientType][msgType]
65+
return operator + strings.TrimPrefix(username, msgType)
66+
}
67+
68+
func selectClientType(recipient string) (string, error) {
69+
at := strings.Split(recipient, "@")
70+
if len(at) != 2 {
71+
return "", fmt.Errorf("invalid recipient: %s", recipient)
72+
}
73+
74+
domain := at[1]
75+
if domain == slackRecipient {
76+
return slackRecipient, nil
77+
}
78+
79+
return emailRecipient, nil
80+
}
81+
82+
func selectClient(clientType string, cfg *Config) (client, error) {
83+
switch clientType {
84+
case emailRecipient:
85+
return newMailClient(cfg)
86+
case slackRecipient:
87+
return newSlackClient(slack.New(cfg.SlackToken)), nil
88+
default:
89+
return nil, fmt.Errorf("unknown client type: %s", clientType)
90+
}
91+
}
92+
93+
func groupByClientType(recipients []string) (map[string][]string, error) {
94+
groups := make(map[string][]string)
95+
96+
for _, recipient := range recipients {
97+
clientType, err := selectClientType(recipient)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to select client type: %w", err)
100+
}
101+
102+
groups[clientType] = append(groups[clientType], recipient)
103+
}
104+
105+
return groups, nil
106+
}

0 commit comments

Comments
 (0)