-
Notifications
You must be signed in to change notification settings - Fork 83
Allow custom email server for invitations #796
Changes from 1 commit
02454cf
2e4e5d2
98d23bb
b6c467e
ac9b8b3
4f85a77
559930a
665c898
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package email | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"firebase.google.com/go/auth" | ||
"github.com/google/exposure-notifications-server/pkg/secrets" | ||
) | ||
|
||
// ProviderType represents a type of email provider. | ||
type ProviderType string | ||
|
||
const ( | ||
ProviderTypeNoop ProviderType = "NOOP" | ||
ProviderTypeFirebase ProviderType = "FIREBASE" | ||
ProviderTypeSmtp ProviderType = "SIMPLE_SMTP" | ||
) | ||
|
||
// Config represents the env var based configuration for email SMTP server connection. | ||
type Config struct { | ||
ProviderType ProviderType | ||
|
||
User string `env:"EMAIL_USER" json:",omitempty"` | ||
Password string `env:"EMAIL_PASSWORD" json:",omitempty"` | ||
SmtpHost string `env:"EMAIL_SMTP_HOST" json:",omitempty"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dunno why these are separate, especially when you just combine them later.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lines like |
||
SmtpPort string `env:"EMAIL_SMTP_PORT" json:",omitempty"` | ||
|
||
// Secrets is the secret configuration. This is used to resolve values that | ||
// are actually pointers to secrets before returning them to the caller. The | ||
// table implementation is the source of truth for which values are secrets | ||
// and which are plaintext. | ||
Secrets secrets.Config | ||
} | ||
|
||
type Provider interface { | ||
// SendNewUserInvitation sends an invite to join the server. | ||
SendNewUserInvitation(ctx context.Context, email string) error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would probably be better to abstract the email content from the email sending. I'd expect the email provider to only know how to type Provider interface {
SendEmail(ctx context.Context, subject, to, cc, bcc string, body io.Reader)
} |
||
} | ||
|
||
func ProviderFor(ctx context.Context, c *Config, auth *auth.Client) (Provider, error) { | ||
switch typ := c.ProviderType; typ { | ||
case ProviderTypeFirebase: | ||
return NewFirebase(ctx) | ||
case ProviderTypeSmtp: | ||
return NewSmtp(ctx, c.User, c.Password, c.SmtpHost, c.SmtpPort, auth) | ||
default: | ||
return nil, fmt.Errorf("unknown email provider type: %v", typ) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package email is logic for sending email invitations | ||
package email | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/google/exposure-notifications-verification-server/internal/firebase" | ||
) | ||
|
||
var _ Provider = (*firebase.Client)(nil) | ||
|
||
// NewFirebase creates a new Smtp email sender with the given auth. | ||
func NewFirebase(ctx context.Context) (Provider, error) { | ||
firebaseInternal, err := firebase.New(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to configure internal firebase client: %w", err) | ||
} | ||
return firebaseInternal, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package email is logic for sending email invitations | ||
package email | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"mime/quotedprintable" | ||
"net/smtp" | ||
|
||
"firebase.google.com/go/auth" | ||
) | ||
|
||
var _ Provider = (*SmtpProvider)(nil) | ||
|
||
// Smtp sends messages via an external SMTP server. | ||
type SmtpProvider struct { | ||
FirebaseAuth *auth.Client | ||
|
||
User string | ||
Password string | ||
SmtpHost string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
SmtpPort string | ||
} | ||
|
||
// NewSmtp creates a new Smtp email sender with the given auth. | ||
func NewSmtp(ctx context.Context, user, password, host, port string, auth *auth.Client) (Provider, error) { | ||
return &SmtpProvider{ | ||
FirebaseAuth: auth, | ||
User: user, | ||
Password: password, | ||
SmtpHost: host, | ||
SmtpPort: port, | ||
}, nil | ||
} | ||
|
||
// SendNewUserInvitation sends a password reset email to the user. | ||
func (s *SmtpProvider) SendNewUserInvitation(ctx context.Context, toEmail string) error { | ||
// Header | ||
header := make(map[string]string) | ||
header["From"] = s.User | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should try to make most of these constants. Both the map keys and many of their values are constant. |
||
header["To"] = toEmail | ||
header["Subject"] = "COVID-19 Verification Server Invitation" | ||
|
||
header["MIME-Version"] = "1.0" | ||
header["Content-Type"] = fmt.Sprintf("%s; charset=\"utf-8\"", "text/html") | ||
whaught marked this conversation as resolved.
Show resolved
Hide resolved
|
||
header["Content-Disposition"] = "inline" | ||
header["Content-Transfer-Encoding"] = "quoted-printable" | ||
|
||
headerMessage := "" | ||
for key, value := range header { | ||
headerMessage += fmt.Sprintf("%s: %s\r\n", key, value) | ||
} | ||
|
||
inviteLink, err := s.FirebaseAuth.PasswordResetLink(ctx, toEmail) | ||
if err != nil { | ||
return err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrap There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bump |
||
} | ||
|
||
// Message. | ||
body := fmt.Sprintf( | ||
"You've been invited to the COVID-19 Verification Server. Use the link below to set up your account."+ | ||
" \n\n %s", inviteLink) | ||
var bodyMessage bytes.Buffer | ||
temp := quotedprintable.NewWriter(&bodyMessage) | ||
temp.Write([]byte(body)) | ||
temp.Close() | ||
|
||
finalMessage := headerMessage + "\r\n" + bodyMessage.String() | ||
|
||
// Authentication. | ||
auth := smtp.PlainAuth("", s.User, s.Password, s.SmtpHost) | ||
|
||
// Sending email. | ||
err = smtp.SendMail(s.SmtpHost+":"+s.SmtpPort, auth, s.User, []string{toEmail}, []byte(finalMessage)) | ||
if err != nil { | ||
return err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrap, or don't bother setting err. |
||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dunno if we want to wrap errors. We're typically trying to.