Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

smtp + email confirmation #285

Merged
merged 45 commits into from
Oct 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2cb8a3b
add smtp configuration
tsmethurst Oct 16, 2021
f53bbfd
add email confirm + reset templates
tsmethurst Oct 16, 2021
3b31e55
add email sender to testrig
tsmethurst Oct 16, 2021
c868cc3
flesh out the email sender interface
tsmethurst Oct 16, 2021
ba1c49b
go fmt
tsmethurst Oct 16, 2021
bf6da31
golint
tsmethurst Oct 16, 2021
f253fce
update from field with more clarity
tsmethurst Oct 16, 2021
2eda7f4
tidy up the email formatting
tsmethurst Oct 16, 2021
2ca3322
fix tests
tsmethurst Oct 16, 2021
5f6a0cd
add email sender to processor
tsmethurst Oct 17, 2021
ec4b3c9
tidy client api processing a bit
tsmethurst Oct 17, 2021
784b648
further tidying in fromClientAPI
tsmethurst Oct 17, 2021
556ac3d
pin new account to user
tsmethurst Oct 17, 2021
212fd84
send msg to processor on new account creation
tsmethurst Oct 17, 2021
1cc9742
generate confirm email uri
tsmethurst Oct 17, 2021
0fee68f
remove emailer from account processor again
tsmethurst Oct 17, 2021
a558cb1
add processCreateAccountFromClientAPI
tsmethurst Oct 17, 2021
49a5e48
move emailer accountprocessor => userprocessor
tsmethurst Oct 17, 2021
55b643c
add email sender to user processor
tsmethurst Oct 17, 2021
a455c01
SendConfirmEmail function
tsmethurst Oct 17, 2021
b0cc815
add noop email sender
tsmethurst Oct 17, 2021
8fdb325
use noop email sender in tests
tsmethurst Oct 17, 2021
b524b12
only assemble message if callback is not nil
tsmethurst Oct 17, 2021
42881aa
use noop email sender if no smtp host is defined
tsmethurst Oct 17, 2021
d3a80b6
minify email html before sending
tsmethurst Oct 17, 2021
4bb1e90
fix wrong email address
tsmethurst Oct 17, 2021
11a53da
email confirm test
tsmethurst Oct 17, 2021
04059f8
fmt
tsmethurst Oct 17, 2021
d1c271f
serve web hndler
tsmethurst Oct 18, 2021
652b41d
add email confirm handler
tsmethurst Oct 18, 2021
88e27da
init test log properly on testrig
tsmethurst Oct 18, 2021
6ee46db
log emails that *would* have been sent
tsmethurst Oct 18, 2021
92ac358
go fmt ./...
tsmethurst Oct 18, 2021
91819ea
unexport confirm email handler
tsmethurst Oct 18, 2021
7caf98a
updatedAt
tsmethurst Oct 18, 2021
141f985
test confirm email function
tsmethurst Oct 18, 2021
3382f4d
don't allow tokens older than 7 days
tsmethurst Oct 18, 2021
e94c686
change error message a bit
tsmethurst Oct 18, 2021
478ddc7
add basic smtp docs
tsmethurst Oct 18, 2021
667e3ca
add a few more snippets
tsmethurst Oct 18, 2021
d0fce9b
typo
tsmethurst Oct 18, 2021
ef036fe
Merge branch 'main' into smtp
tsmethurst Oct 24, 2021
9c7e063
add email sender to outbox tests
tsmethurst Oct 24, 2021
1470202
don't use dutch wikipedia link
tsmethurst Oct 31, 2021
edfc7fe
don't minify email html
tsmethurst Oct 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/gotosocial/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func getFlags() []cli.Flag {
statusesFlags(flagNames, envNames, defaults),
letsEncryptFlags(flagNames, envNames, defaults),
oidcFlags(flagNames, envNames, defaults),
smtpFlags(flagNames, envNames, defaults),
}
for _, fs := range flagSets {
flags = append(flags, fs...)
Expand Down
59 changes: 59 additions & 0 deletions cmd/gotosocial/smtpflags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors [email protected]

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package main

import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/urfave/cli/v2"
)

func smtpFlags(flagNames, envNames config.Flags, defaults config.Defaults) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: flagNames.SMTPHost,
Usage: "Host of the smtp server. Eg., 'smtp.eu.mailgun.org'",
Value: defaults.SMTPHost,
EnvVars: []string{envNames.SMTPHost},
},
&cli.IntFlag{
Name: flagNames.SMTPPort,
Usage: "Port of the smtp server. Eg., 587",
Value: defaults.SMTPPort,
EnvVars: []string{envNames.SMTPPort},
},
&cli.StringFlag{
Name: flagNames.SMTPUsername,
Usage: "Username to authenticate with the smtp server as. Eg., '[email protected]'",
Value: defaults.SMTPUsername,
EnvVars: []string{envNames.SMTPUsername},
},
&cli.StringFlag{
Name: flagNames.SMTPPassword,
Usage: "Password to pass to the smtp server.",
Value: defaults.SMTPPassword,
EnvVars: []string{envNames.SMTPPassword},
},
&cli.StringFlag{
Name: flagNames.SMTPFrom,
Usage: "Address to use as the 'from' field of the email. Eg., '[email protected]'",
Value: defaults.SMTPFrom,
EnvVars: []string{envNames.SMTPFrom},
},
}
}
67 changes: 67 additions & 0 deletions docs/configuration/smtp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Email Config (smtp)

GoToSocial supports sending emails to users via the [Simple Mail Transfer Protocol](https://wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) or **smtp**.

Configuring GoToSocial to send emails is **not required** in order to have a properly running instance. Still, it's very useful for doing things like sending confirmation emails and notifications, and handling password reset requests.

In order to make GoToSocial email sending work, you need an smtp-compatible mail service running somewhere, either as a server on the same machine that GoToSocial is running on, or via an external service like [Mailgun](https://mailgun.com). It may also be possible to use a free personal email address for sending emails, if your email provider supports smtp (check with them--most do), but you might run into trouble sending lots of emails.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add config option to ratelimit amount of emails sent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do this in a separate PR later I think. Rate limiting is something we haven't touched yet anywhere, so there's probably a lot of reusable logic we can write for that (write one rate-limiter interface and attach it to everything that needs to be rate limited, for instance).


## Settings

The configuration options for smtp are as follows:

```yaml
#######################
##### SMTP CONFIG #####
#######################

# Config for sending emails via an smtp server. See https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
smtp:

# String. The hostname of the smtp server you want to use.
# If this is not set, smtp will not be used to send emails, and you can ignore the other settings.
# Examples: ["mail.example.org", "localhost"]
# Default: ""
host: ""
# Int. Port to use to connect to the smtp server.
# Examples: []
# Default: 0
port: 0
# String. Username to use when authenticating with the smtp server.
# This should have been provided to you by your smtp host.
# This is often, but not always, an email address.
# Examples: ["[email protected]"]
# Default: ""
username:
# String. Password to use when authenticating with the smtp server.
# This should have been provided to you by your smtp host.
# Examples: ["1234", "password"]
# Default: ""
password:
# String. 'From' address for sent emails.
# Examples: ["[email protected]"]
# Default: ""
from: ""
```

Note that if you don't set `Host`, then email sending via smtp will be disabled, and the other settings will be ignored. GoToSocial will still log (at trace level) emails that *would* have been sent if smtp was enabled.

## Behavior

### SSL

GoToSocial requires your smtp server to present valid SSL certificates. Most of the big services like Mailgun do this anyway, but if you're running your own mail server without SSL for some reason, and you're trying to connect GoToSocial to it, it will not work.

The exception to this requirement is if you're running your mail server (or bridge to a mail server) on `localhost`, in which case SSL certs are not required.

### When are emails sent?

Currently, emails are only sent to users to request email confirmation when a new account is created, or to serve password reset requests. More email functionality will probably be added later.

### HTML versus Plaintext

Emails are sent in HTML by default. At this point, there is no option to send emails in plaintext, but this is something that might be added later if there's enough demand for it.

## Customization

If you like, you can customize the templates that are used for generating emails. Follow the examples in `web/templates`.
32 changes: 32 additions & 0 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,35 @@ oidc:
- "email"
- "profile"
- "groups"

#######################
##### SMTP CONFIG #####
#######################

# Config for sending emails via an smtp server. See https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
smtp:

# String. The hostname of the smtp server you want to use.
# If this is not set, smtp will not be used to send emails, and you can ignore the other settings.
# Examples: ["mail.example.org", "localhost"]
# Default: ""
host: ""
# Int. Port to use to connect to the smtp server.
# Examples: []
# Default: 0
port: 0
# String. Username to use when authenticating with the smtp server.
# This should have been provided to you by your smtp host.
# This is often, but not always, an email address.
# Examples: ["[email protected]"]
# Default: ""
username:
# String. Password to use when authenticating with the smtp server.
# This should have been provided to you by your smtp host.
# Examples: ["1234", "password"]
# Default: ""
password:
# String. 'From' address for sent emails.
# Examples: ["[email protected]"]
# Default: ""
from: ""
19 changes: 12 additions & 7 deletions internal/api/client/account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
Expand All @@ -23,12 +24,14 @@ import (
type AccountStandardTestSuite struct {
// standard suite interfaces
suite.Suite
config *config.Config
db db.DB
tc typeutils.TypeConverter
storage *kv.KVStore
federator federation.Federator
processor processing.Processor
config *config.Config
db db.DB
tc typeutils.TypeConverter
storage *kv.KVStore
federator federation.Federator
processor processing.Processor
emailSender email.Sender
sentEmails map[string]string

// standard suite models
testTokens map[string]*gtsmodel.Token
Expand Down Expand Up @@ -59,7 +62,9 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.storage = testrig.NewTestStorage()
testrig.InitTestLog()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
suite.sentEmails = make(map[string]string)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender)
suite.accountModule = account.New(suite.config, suite.processor).(*account.Module)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
Expand Down
6 changes: 5 additions & 1 deletion internal/api/client/fileserver/servefile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
Expand All @@ -54,6 +55,7 @@ type ServeFileTestSuite struct {
processor processing.Processor
mediaHandler media.Handler
oauthServer oauth.Server
emailSender email.Sender

// standard suite models
testTokens map[string]*gtsmodel.Token
Expand All @@ -78,7 +80,9 @@ func (suite *ServeFileTestSuite) SetupSuite() {
testrig.InitTestLog()
suite.storage = testrig.NewTestStorage()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)

suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender)
suite.tc = testrig.NewTestTypeConverter(suite.db)
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
Expand Down
15 changes: 9 additions & 6 deletions internal/api/client/followrequest/followrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequest"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
Expand All @@ -38,11 +39,12 @@ import (

type FollowRequestStandardTestSuite struct {
suite.Suite
config *config.Config
db db.DB
storage *kv.KVStore
federator federation.Federator
processor processing.Processor
config *config.Config
db db.DB
storage *kv.KVStore
federator federation.Federator
processor processing.Processor
emailSender email.Sender

// standard suite models
testTokens map[string]*gtsmodel.Token
Expand Down Expand Up @@ -73,7 +75,8 @@ func (suite *FollowRequestStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB()
suite.storage = testrig.NewTestStorage()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender)
suite.followRequestModule = followrequest.New(suite.config, suite.processor).(*followrequest.Module)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
Expand Down
5 changes: 4 additions & 1 deletion internal/api/client/media/mediacreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
Expand All @@ -56,6 +57,7 @@ type MediaCreateTestSuite struct {
tc typeutils.TypeConverter
mediaHandler media.Handler
oauthServer oauth.Server
emailSender email.Sender
processor processing.Processor

// standard suite models
Expand Down Expand Up @@ -84,7 +86,8 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender)

// setup module being tested
suite.mediaModule = mediamodule.New(suite.config, suite.processor).(*mediamodule.Module)
Expand Down
45 changes: 38 additions & 7 deletions internal/api/client/status/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)

// nolint
type StatusStandardTestSuite struct {
// standard suite interfaces
suite.Suite
config *config.Config
db db.DB
tc typeutils.TypeConverter
federator federation.Federator
processor processing.Processor
storage *kv.KVStore
config *config.Config
db db.DB
tc typeutils.TypeConverter
federator federation.Federator
emailSender email.Sender
processor processing.Processor
storage *kv.KVStore

// standard suite models
testTokens map[string]*gtsmodel.Token
Expand All @@ -53,3 +55,32 @@ type StatusStandardTestSuite struct {
// module being tested
statusModule *status.Module
}

func (suite *StatusStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
suite.testAttachments = testrig.NewTestAttachments()
suite.testStatuses = testrig.NewTestStatuses()
}

func (suite *StatusStandardTestSuite) SetupTest() {
suite.config = testrig.NewTestConfig()
suite.db = testrig.NewTestDB()
suite.tc = testrig.NewTestTypeConverter(suite.db)
suite.storage = testrig.NewTestStorage()
testrig.InitTestLog()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender)
suite.statusModule = status.New(suite.config, suite.processor).(*status.Module)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
}

func (suite *StatusStandardTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
}
Loading