From 3ebcd95c9dc21fee4731f17f3968d190ffe5832d Mon Sep 17 00:00:00 2001 From: Amitai Burstein Date: Thu, 15 Feb 2024 11:55:17 +0200 Subject: [PATCH] Allow easier SMTP config envs (#1910) * Easier import of SMTP credentials * EnvVarReader SMTPEncryption * Add docs * Add comments --- Guide/mail.markdown | 102 +++++++++++++++++++++++++++++++++++--------- IHP/EnvVar.hs | 21 ++++++++- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/Guide/mail.markdown b/Guide/mail.markdown index def5a1ca1..fc61aff73 100644 --- a/Guide/mail.markdown +++ b/Guide/mail.markdown @@ -144,12 +144,16 @@ Remember that the successfull delivery of email largely depends on the from-doma The delivery method is set in `Config/Config.hs` as shown below. -### Any SMTP Server +### Set SMTP by Environment Variables + +It's a good idea to not hardcode your SMTP credentials in your code. Instead, you can use environment variables to set your SMTP credentials. This is especially useful when deploying your application to a cloud provider like AWS, where it will use different credentials in production than on your local machine. + ```haskell -- Add this import import IHP.Mail + config :: ConfigBuilder config = do -- other options here, then add: @@ -163,7 +167,7 @@ config = do ### Local SMTP with Mailhog -A convinient way to see sent mails is to use a local mail testing such as [MailHog](https://github.com/mailhog/MailHog). This service will catch all outgoing emails, and show their HTML to you - which is handy while developing. +A convenient way to see sent mails is to use a local mail testing such as [MailHog](https://github.com/mailhog/MailHog). This service will catch all outgoing emails, and show their HTML to you - which is handy while developing. 1. Make sure `sendmail` is locally installed and configured. 2. Install MailHog. @@ -173,21 +177,80 @@ A convinient way to see sent mails is to use a local mail testing such as [MailH ```haskell --- Add this import +-- Config/Config.hs + +-- Add these imports import IHP.Mail +import Network.Socket (PortNumber) +import IHP.Mail.Types (SMTPEncryption) +import IHP.EnvVar config :: ConfigBuilder config = do - -- other options here, then add: - option $ SMTP - { host = "127.0.1.1" -- On some computers may need `127.0.0.1` instead. - , port = 1025 - , credentials = Nothing - , encryption = Unencrypted - } + -- Previous options + -- ... + + -- Read SMTP configuration from environment variables. + smtpHost <- env @Text "SMTP_HOST" + smtpPort <- env @PortNumber "SMTP_PORT" + smtpEncryption <- env @SMTPEncryption "SMTP_ENCRYPTION" + + smtpUserMaybe <- envOrNothing "SMTP_USER" + smtpPasswordMaybe <- envOrNothing "SMTP_PASSWORD" + + -- Determine if credentials are available and set SMTP configuration accordingly. + let smtpCredentials = case (smtpUserMaybe, smtpPasswordMaybe) of + (Just user, Just password) -> Just (user, password) + _ -> Nothing + + -- Print out SMTP configuration for debugging purposes. + liftIO $ putStrLn $ "SMTP HOST: " <> show smtpHost + liftIO $ putStrLn $ "SMTP PORT: " <> show smtpPort + + -- SMTP to work with MailHog or other SMTP services. + option $ + SMTP + { host = cs smtpHost + , port = smtpPort + , credentials = smtpCredentials + , encryption = smtpEncryption + } ``` -### SendGrid +Then set the environment variables in your `.envrc` file: + +``` +# Add your env vars here +# +# E.g. export AWS_ACCESS_KEY_ID="XXXXX" +export SMTP_HOST="127.0.0.1" # On some computers may need `127.0.1.1` instead. +export SMTP_PORT="1025" +export SMTP_ENCRYPTION="Unencrypted" +``` + +Finally, when you'll have the app deployed, for example on AWS using SES, you would have to set the environment variables in your `flake.nix`. + +```nix +services.ihp = { + domain = "..."; + migrations = ./Application/Migration; + schema = ./Application/Schema.sql; + fixtures = ./Application/Fixtures.sql; + sessionSecret = "..."; + additionalEnvVars = { + SMTP_HOST = "email-smtp.eu-west-1.amazonaws.com"; + SMTP_PORT = "587"; + SMTP_ENCRYPTION = "STARTTLS"; + SMTP_USER = "your-user-name"; + SMTP_PASSWORD = "your-password"; + ENV_NAME = "qa"; + AWS_ACCESS_KEY_ID = "your key ID"; + AWS_SECRET_ACCESS_KEY = "your secret"; + }; +}; +``` + +### AWS SES ```haskell -- Add this import @@ -196,14 +259,14 @@ import IHP.Mail config :: ConfigBuilder config = do -- other options here, then add: - option $ SendGrid - { apiKey = "YOUR SENDGRID API KEY" - , category = Nothing -- or Just "mailcategory" + option $ SES + { accessKey = "YOUR AWS ACCESS KEY" + , secretKey = "YOUR AWS SECRET KEY" + , region = "eu-west-1" -- YOUR REGION } ``` - -### AWS SES +### SendGrid ```haskell -- Add this import @@ -212,10 +275,9 @@ import IHP.Mail config :: ConfigBuilder config = do -- other options here, then add: - option $ SES - { accessKey = "YOUR AWS ACCESS KEY" - , secretKey = "YOUR AWS SECRET KEY" - , region = "eu-west-1" -- YOUR REGION + option $ SendGrid + { apiKey = "YOUR SENDGRID API KEY" + , category = Nothing -- or Just "mailcategory" } ``` diff --git a/IHP/EnvVar.hs b/IHP/EnvVar.hs index b274deb4a..a2e164bb8 100644 --- a/IHP/EnvVar.hs +++ b/IHP/EnvVar.hs @@ -10,6 +10,9 @@ where import IHP.Prelude import Data.String.Interpolate.IsString (i) import qualified System.Posix.Env.ByteString as Posix +import Network.Socket (PortNumber) +import Data.Word (Word16) +import IHP.Mail.Types import IHP.Environment -- | Returns a env variable. The raw string @@ -88,4 +91,20 @@ instance EnvVarReader ByteString where instance EnvVarReader Bool where envStringToValue "1" = Right True envStringToValue "0" = Right False - envStringToValue otherwise = Left "Should be set to '1' or '0'" \ No newline at end of file + envStringToValue otherwise = Left "Should be set to '1' or '0'" + +-- | Allow reading the env var of an SMTP port number. +instance EnvVarReader PortNumber where + envStringToValue string = case textToInt (cs string) of + Just integer -> Right $ convertIntToPortNumber integer + Nothing -> Left [i|Expected integer to be used as a Port number, got #{string}|] + +convertIntToPortNumber :: Int -> PortNumber +convertIntToPortNumber int = fromIntegral (int :: Int) :: PortNumber + +-- | Allow reading the env var of an SMTP encryption method. +instance EnvVarReader SMTPEncryption where + envStringToValue "Unencrypted" = Right Unencrypted + envStringToValue "TLS" = Right TLS + envStringToValue "STARTTLS" = Right STARTTLS + envStringToValue otherwise = Left [i|Expected 'Unencrypted', 'TLS' or 'STARTTLS', got #{otherwise}|] \ No newline at end of file