[server] Add optional TLS/SSL encryption for SMTP (#6863)
## Description Implement TLS/SSL encryption for sending emails via SMTP. When an SMTP provider explicitly requires TLS/SSL communication the current implementation runs in a timeout and fails. A new configuration parameter for smtp was added to enable TLS/SSL communication. This would solve #5958 ## Tests I built a local docker image of my branch. The email provider I was using is mailbox.org and using the tls configuration. Registering a new user then resulted in a sent email containing the verification code. I did not test a setup without TLS/SSL.
This commit is contained in:
@@ -142,6 +142,22 @@ var _updateFreeUserStorage = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var _sendMail = &cobra.Command{
|
||||
Use: "send-mail <to-email> <from-email> <from-name>",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Short: "Sends a test mail via the admin api",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
return ctrl.SendTestMail(context.Background(), *flags, args[0], args[1], args[2])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(_adminCmd)
|
||||
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
|
||||
@@ -159,5 +175,6 @@ func init() {
|
||||
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
|
||||
// add a flag with no value --no-limit
|
||||
_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser)
|
||||
_sendMail.Flags().StringP("admin-user", "a", "", "The email of the admin user. ")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser, _sendMail)
|
||||
}
|
||||
|
||||
@@ -139,5 +139,28 @@ func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.User
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) SendTestMail(ctx context.Context, toEmail, fromEmail, fromName string) error {
|
||||
body := map[string]interface{}{
|
||||
"to": []string{toEmail},
|
||||
"fromName": fromName,
|
||||
"fromEmail": fromEmail,
|
||||
"subject": "Test mail from Ente",
|
||||
"body": "This is a test mail from Ente",
|
||||
}
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetBody(body).
|
||||
Post("/admin/mail")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IsError() {
|
||||
return &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,6 +156,23 @@ func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActio
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) SendTestMail(ctx context.Context, params model.AdminActionForUser, to, from, fromName string) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client.SendTestMail(accountCtx, to, from, fromName)
|
||||
if err != nil {
|
||||
if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") {
|
||||
fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Successfully sent test email to %s\n", to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -171,16 +171,19 @@ smtp:
|
||||
email:
|
||||
# Optional name for sender
|
||||
sender-name:
|
||||
# Optional encryption
|
||||
encryption:
|
||||
```
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------ | ---------------------------- | ------- |
|
||||
|--------------------|------------------------------| ------- |
|
||||
| `smtp.host` | SMTP server host | |
|
||||
| `smtp.port` | SMTP server port | |
|
||||
| `smtp.username` | SMTP auth username | |
|
||||
| `smtp.password` | SMTP auth password | |
|
||||
| `smtp.email` | Sender email address | |
|
||||
| `smtp.sender-name` | Custom name for email sender | |
|
||||
| `smtp.encryption` | Encryption method (tls, ssl) | |
|
||||
| `transmail.key` | Zeptomail API key | |
|
||||
|
||||
### WebAuthn Passkey Support
|
||||
|
||||
@@ -255,6 +255,8 @@ smtp:
|
||||
# Optional override for the sender name in the emails. If specified, it will
|
||||
# be used for all emails sent by the instance (default is email specific).
|
||||
sender-name:
|
||||
# Optional encryption method. If tls or ssl is chosen, encryption is enabled.
|
||||
encryption:
|
||||
|
||||
# Zoho Zeptomail config (optional)
|
||||
#
|
||||
|
||||
@@ -7,6 +7,7 @@ package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -47,6 +48,7 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s
|
||||
smtpPassword := viper.GetString("smtp.password")
|
||||
smtpEmail := viper.GetString("smtp.email")
|
||||
smtpSenderName := viper.GetString("smtp.sender-name")
|
||||
smtpEncryption := viper.GetString("smtp.encryption")
|
||||
|
||||
var emailMessage string
|
||||
var auth smtp.Auth = nil
|
||||
@@ -104,7 +106,7 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s
|
||||
|
||||
// Send the email to each recipient
|
||||
for _, toEmail := range toEmails {
|
||||
err := smtp.SendMail(smtpServer+":"+smtpPort, auth, fromEmail, []string{toEmail}, []byte(emailMessage))
|
||||
err := sendMailWithEncryption(smtpServer, smtpPort, auth, fromEmail, []string{toEmail}, []byte(emailMessage), smtpEncryption)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
for i := range knownInvalidEmailErrors {
|
||||
@@ -119,6 +121,76 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendMailWithEncryption sends an email with the specified encryption type
|
||||
// encryption can be one of:
|
||||
// - "tls" or "ssl": Uses TLS/SSL encryption for the entire connection
|
||||
// - "" (empty string) or any other value: No encryption
|
||||
func sendMailWithEncryption(host, port string, auth smtp.Auth, from string, to []string, msg []byte, encryption string) error {
|
||||
addr := host + ":" + port
|
||||
|
||||
switch strings.ToLower(encryption) {
|
||||
case "tls", "ssl":
|
||||
// For TLS/SSL, establish a secure connection directly
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: host,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to establish TLS connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to create SMTP client over TLS")
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return sendWithClient(client, auth, from, to, msg)
|
||||
|
||||
default:
|
||||
// No encryption, use standard SendMail
|
||||
return smtp.SendMail(addr, auth, from, to, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// sendWithClient sends an email using an established SMTP client
|
||||
func sendWithClient(client *smtp.Client, auth smtp.Auth, from string, to []string, msg []byte) error {
|
||||
if auth != nil {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return stacktrace.Propagate(err, "authentication failed")
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
return stacktrace.Propagate(err, "failed to set sender")
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err := client.Rcpt(addr); err != nil {
|
||||
return stacktrace.Propagate(err, "failed to add recipient")
|
||||
}
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to create message writer")
|
||||
}
|
||||
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to write message")
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return stacktrace.Propagate(err, "failed to close message writer")
|
||||
}
|
||||
|
||||
err = client.Quit()
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
func sendViaTransmail(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error {
|
||||
if len(toEmails) == 0 {
|
||||
return ente.ErrBadRequest
|
||||
|
||||
Reference in New Issue
Block a user