[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() {
|
func init() {
|
||||||
rootCmd.AddCommand(_adminCmd)
|
rootCmd.AddCommand(_adminCmd)
|
||||||
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
|
_ = _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)")
|
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
|
||||||
// add a flag with no value --no-limit
|
// 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")
|
_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
|
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
|
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) {
|
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
|
||||||
accounts, err := c.GetAccounts(ctx)
|
accounts, err := c.GetAccounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -171,16 +171,19 @@ smtp:
|
|||||||
email:
|
email:
|
||||||
# Optional name for sender
|
# Optional name for sender
|
||||||
sender-name:
|
sender-name:
|
||||||
|
# Optional encryption
|
||||||
|
encryption:
|
||||||
```
|
```
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| ------------------ | ---------------------------- | ------- |
|
|--------------------|------------------------------| ------- |
|
||||||
| `smtp.host` | SMTP server host | |
|
| `smtp.host` | SMTP server host | |
|
||||||
| `smtp.port` | SMTP server port | |
|
| `smtp.port` | SMTP server port | |
|
||||||
| `smtp.username` | SMTP auth username | |
|
| `smtp.username` | SMTP auth username | |
|
||||||
| `smtp.password` | SMTP auth password | |
|
| `smtp.password` | SMTP auth password | |
|
||||||
| `smtp.email` | Sender email address | |
|
| `smtp.email` | Sender email address | |
|
||||||
| `smtp.sender-name` | Custom name for email sender | |
|
| `smtp.sender-name` | Custom name for email sender | |
|
||||||
|
| `smtp.encryption` | Encryption method (tls, ssl) | |
|
||||||
| `transmail.key` | Zeptomail API key | |
|
| `transmail.key` | Zeptomail API key | |
|
||||||
|
|
||||||
### WebAuthn Passkey Support
|
### WebAuthn Passkey Support
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ smtp:
|
|||||||
# Optional override for the sender name in the emails. If specified, it will
|
# 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).
|
# be used for all emails sent by the instance (default is email specific).
|
||||||
sender-name:
|
sender-name:
|
||||||
|
# Optional encryption method. If tls or ssl is chosen, encryption is enabled.
|
||||||
|
encryption:
|
||||||
|
|
||||||
# Zoho Zeptomail config (optional)
|
# Zoho Zeptomail config (optional)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@@ -47,6 +48,7 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s
|
|||||||
smtpPassword := viper.GetString("smtp.password")
|
smtpPassword := viper.GetString("smtp.password")
|
||||||
smtpEmail := viper.GetString("smtp.email")
|
smtpEmail := viper.GetString("smtp.email")
|
||||||
smtpSenderName := viper.GetString("smtp.sender-name")
|
smtpSenderName := viper.GetString("smtp.sender-name")
|
||||||
|
smtpEncryption := viper.GetString("smtp.encryption")
|
||||||
|
|
||||||
var emailMessage string
|
var emailMessage string
|
||||||
var auth smtp.Auth = nil
|
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
|
// Send the email to each recipient
|
||||||
for _, toEmail := range toEmails {
|
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 {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
errMsg := err.Error()
|
||||||
for i := range knownInvalidEmailErrors {
|
for i := range knownInvalidEmailErrors {
|
||||||
@@ -119,6 +121,76 @@ func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject s
|
|||||||
return nil
|
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 {
|
func sendViaTransmail(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error {
|
||||||
if len(toEmails) == 0 {
|
if len(toEmails) == 0 {
|
||||||
return ente.ErrBadRequest
|
return ente.ErrBadRequest
|
||||||
|
|||||||
Reference in New Issue
Block a user