Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fc3a6ed
Add more logging and error handling for SMTP
supersven Oct 28, 2022
c488acc
Catch in ensureSMTPConnectionTimeout
supersven Oct 28, 2022
38e2844
Improve logging
supersven Oct 28, 2022
29ad2a0
Remove unnecessary do-s
supersven Oct 28, 2022
56f795d
Add type signatures
supersven Oct 28, 2022
eeb01df
Simplify expressions
supersven Oct 28, 2022
380ee8e
Guard closing of the test connection
supersven Oct 28, 2022
54fd4ae
Better log prefix strings
supersven Oct 31, 2022
9803807
Add postie to brig integration test dependencies
supersven Nov 2, 2022
5be3c1a
Add first SMTP test
supersven Nov 2, 2022
1e39bb8
Adjust log strings
supersven Nov 2, 2022
24b8b60
Good case: Receive mail via SMTP
supersven Nov 2, 2022
3313c65
Ensure Subject is sent/received
supersven Nov 2, 2022
fd77321
Add TODO
supersven Nov 2, 2022
2911b00
Test failed SMTP transaction
supersven Nov 2, 2022
a05af68
Formatting...
supersven Nov 2, 2022
ff7c366
Add more tests
supersven Nov 3, 2022
6104137
Better test description
supersven Nov 3, 2022
4970cce
Simplify test setup: Get rid on IO
supersven Nov 3, 2022
88b2ce3
More on tests
supersven Nov 3, 2022
741913e
Flush log before error handling
supersven Nov 3, 2022
54d6ec1
Resolve TODOs
supersven Nov 3, 2022
f52da62
Add changelog
supersven Nov 3, 2022
c8f9fec
Use random port for SMTP server
supersven Nov 4, 2022
5971c6a
Reduce test setup duplication
supersven Nov 4, 2022
41301e3
Re-order functions
supersven Nov 4, 2022
beff8f3
Ensure that test mail server ports aren't priviledged and not already…
supersven Nov 4, 2022
1d194b6
Final touches to log/error messages
supersven Nov 4, 2022
a302e6a
Add missing Nix dependencies
supersven Nov 11, 2022
d614e75
Stricter version definition for postie
supersven Nov 21, 2022
263a8cd
Use Log.field to combine log items
supersven Nov 21, 2022
75b55dc
Add haddock
supersven Nov 21, 2022
b8366ba
Simplify: Get rid of Either
supersven Nov 22, 2022
d133413
Cleanup
supersven Nov 22, 2022
f3e0fc1
Fix typo
supersven Nov 22, 2022
48a481c
Update services/brig/src/Brig/SMTP.hs
supersven Nov 23, 2022
75c8f99
Update services/brig/src/Brig/SMTP.hs
supersven Nov 23, 2022
bae38bf
Reduce duplication in logging
supersven Nov 23, 2022
0acd2d9
Fix test flakiness
supersven Nov 25, 2022
791578b
Ensure sockets are always closed
supersven Nov 25, 2022
75446ea
Fix typo
supersven Nov 28, 2022
9167e10
Remove now unused Arbitrary instance
supersven Dec 21, 2022
16675cf
Sync default.nix with cabal file
supersven Dec 21, 2022
7bd5232
Apply suggestions from code review
supersven Dec 21, 2022
d2e4f64
Apply suggestions from code review
supersven Dec 21, 2022
7224f36
Hlint
supersven Dec 21, 2022
a6d09bc
Add missing import
supersven Dec 21, 2022
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 changelog.d/2-features/smtp-logging
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add more logs to SMTP mail sending. Ensure that logs are written before the application fails due to SMTP misconfiguration.
8 changes: 8 additions & 0 deletions nix/haskell-pins.nix
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ let
tasty-hunit = "hunit";
};
};
# This can be removed once postie 0.6.0.3 (or later) is in nixpkgs
postie = {
src = fetchgit {
url = "https://github.com/alexbiehl/postie.git";
rev = "c92702386f760fcaa65cd052dc8114889c001e3f";
sha256 = "sha256-yiw6hg3guRWS6CVdrUY8wyIDxoqfGjIVMrEtP+Fys0Y=";
};
};
};
hackagePins = {
kind-generics = {
Expand Down
3 changes: 3 additions & 0 deletions nix/manual-overrides.nix
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ hself: hsuper: {

# Make hoogle static to reduce size of the hoogle image
hoogle = hlib.justStaticExecutables hsuper.hoogle;

# Postie has been fixed upstream (master)
postie = hlib.markUnbroken (hlib.doJailbreak hsuper.postie);
}
8 changes: 8 additions & 0 deletions services/brig/brig.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ library
, text >=0.11
, text-icu-translit >=0.1
, time >=1.1
, time-out
, time-units
, tinylog >=0.10
, transformers >=0.3
, types-common >=0.16
Expand Down Expand Up @@ -464,6 +466,7 @@ executable brig-integration
Federation.Util
Index.Create
Main
SMTP
Util
Util.AWS

Expand Down Expand Up @@ -553,13 +556,16 @@ executable brig-integration
, lens-aeson
, metrics-wai
, mime >=0.4
, mime-mail
, MonadRandom >=0.5
, mtl
, network
, optparse-applicative
, pem
, pipes
, polysemy
, polysemy-wire-zoo
, postie >=0.6.0.3
, process
, proto-lens
, QuickCheck
Expand All @@ -573,13 +579,15 @@ executable brig-integration
, servant-client
, servant-client-core
, spar
, streaming-commons
, string-conversions
, tasty >=1.0
, tasty-cannon >=0.3.4
, tasty-hunit >=0.2
, temporary >=1.2.1
, text
, time >=1.5
, time-units
, tinylog
, transformers
, types-common >=0.3
Expand Down
12 changes: 12 additions & 0 deletions services/brig/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@
, network-conduit-tls
, optparse-applicative
, pem
, pipes
, polysemy
, polysemy-plugin
, polysemy-wire-zoo
, postie
, process
, proto-lens
, QuickCheck
Expand Down Expand Up @@ -116,6 +118,7 @@
, ssl-util
, statistics
, stomp-queue
, streaming-commons
, string-conversions
, swagger
, swagger2
Expand All @@ -130,6 +133,8 @@
, text
, text-icu-translit
, time
, time-out
, time-units
, tinylog
, transformers
, types-common
Expand Down Expand Up @@ -267,6 +272,8 @@ mkDerivation {
text
text-icu-translit
time
time-out
time-units
tinylog
transformers
types-common
Expand Down Expand Up @@ -329,13 +336,16 @@ mkDerivation {
lens-aeson
metrics-wai
mime
mime-mail
MonadRandom
mtl
network
optparse-applicative
pem
pipes
polysemy
polysemy-wire-zoo
postie
process
proto-lens
QuickCheck
Expand All @@ -349,13 +359,15 @@ mkDerivation {
servant-client
servant-client-core
spar
streaming-commons
string-conversions
tasty
tasty-cannon
tasty-hunit
temporary
text
time
time-units
tinylog
transformers
types-common
Expand Down
7 changes: 4 additions & 3 deletions services/brig/src/Brig/Email.hs
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,20 @@ module Brig.Email
where

import qualified Brig.AWS as AWS
import Brig.App (Env, awsEnv, smtpEnv)
import Brig.App (Env, applog, awsEnv, smtpEnv)
import qualified Brig.SMTP as SMTP
import Control.Lens (view)
import Control.Monad.Catch
import qualified Data.Text as Text
import Imports
import Network.Mail.Mime
import Wire.API.User

-------------------------------------------------------------------------------
sendMail :: (MonadIO m, MonadReader Env m) => Mail -> m ()
sendMail :: (MonadIO m, MonadCatch m, MonadReader Env m) => Mail -> m ()
sendMail m =
view smtpEnv >>= \case
Just smtp -> SMTP.sendMail smtp m
Just smtp -> view applog >>= \logger -> SMTP.sendMail logger smtp m
Nothing -> view awsEnv >>= \e -> AWS.execute e $ AWS.sendMail m

-------------------------------------------------------------------------------
Expand Down
187 changes: 164 additions & 23 deletions services/brig/src/Brig/SMTP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Brig.SMTP where
module Brig.SMTP
( sendMail,
initSMTP,
sendMail',
initSMTP',
SMTPConnType (..),
SMTP (..),
Username (..),
Password (..),
SMTPPoolException (..),
)
where

import qualified Control.Exception as CE (throw)
import Control.Lens
import Control.Monad.Catch
import Control.Timeout (timeout)
import Data.Aeson
import Data.Aeson.TH
import Data.Pool
import Data.Text (unpack)
import Data.Time.Units
import Imports
import qualified Network.HaskellNet.SMTP as SMTP
import qualified Network.HaskellNet.SMTP.SSL as SMTP
Expand All @@ -50,17 +65,73 @@ deriveJSON defaultOptions {constructorTagModifier = map toLower} ''SMTPConnType

makeLenses ''SMTP

initSMTP :: Logger -> Text -> Maybe PortNumber -> Maybe (Username, Password) -> SMTPConnType -> IO SMTP
initSMTP lg host port credentials connType = do
-- Try to initiate a connection and fail badly right away in case of bad auth
-- otherwise config errors will be detected "too late"
(success, _) <- connect
unless success $
error "Failed to authenticate against the SMTP server"
data SMTPPoolException = SMTPUnauthorized | SMTPConnectionTimeout
deriving (Eq, Show)

instance Exception SMTPPoolException

-- | Initiate the `SMTP` connection pool
--
-- Throws exceptions when the SMTP server is unreachable, authentication fails,
-- a timeout happens and on every other network failure.
Comment thread
supersven marked this conversation as resolved.
Outdated
--
-- `defaultTimeoutDuration` is used as timeout duration for all actions.
initSMTP ::
Logger ->
Text ->
Maybe PortNumber ->
Maybe (Username, Password) ->
SMTPConnType ->
IO SMTP
initSMTP = initSMTP' defaultTimeoutDuration

-- | `initSMTP` with configurable timeout duration
--
-- This is mostly useful for testing. (We don't want to waste the amount of
-- `defaultTimeoutDuration` in tests with waiting.)
initSMTP' ::
(TimeUnit t) =>
t ->
Logger ->
Text ->
Maybe PortNumber ->
Maybe (Username, Password) ->
SMTPConnType ->
IO SMTP
initSMTP' timeoutDuration lg host port credentials connType = do
-- Try to initiate a connection and fail badly right away in case of bad auth.
-- Otherwise, config errors will be detected "too late".
con <-
catch
( logExceptionOrResult
lg
("Checking test connection to " ++ unpack host ++ " on startup")
establishConnection
)
( \(e :: SomeException) -> do
-- Ensure that the logs are written: In case of failure, the error thrown
-- below will kill the app (which could otherwise leave the logs unwritten).
flush lg
error $ "Failed to establish test connection with SMTP server: " ++ show e
)
catch
( logExceptionOrResult lg "Closing test connection on startup" $
ensureSMTPConnectionTimeout timeoutDuration (SMTP.gracefullyCloseSMTP con)
)
( \(e :: SomeException) -> do
-- Ensure that the logs are written: In case of failure, the error thrown
-- below will kill the app (which could otherwise leave the logs unwritten).
flush lg
error $ "Failed to close test connection with SMTP server: " ++ show e
)
SMTP <$> createPool create destroy 1 5 5
where
connect = do
conn <- case (connType, port) of
ensureTimeout :: IO a -> IO a
ensureTimeout = ensureSMTPConnectionTimeout timeoutDuration

establishConnection :: IO SMTP.SMTPConnection
establishConnection = do
conn <- ensureTimeout $ case (connType, port) of
(Plain, Nothing) -> SMTP.connectSMTP (unpack host)
(Plain, Just p) -> SMTP.connectSMTPPort (unpack host) p
(TLS, Nothing) -> SMTP.connectSMTPSTARTTLS (unpack host)
Expand All @@ -72,18 +143,88 @@ initSMTP lg host port credentials connType = do
SMTP.connectSMTPSSLWithSettings (unpack host) $
SMTP.defaultSettingsSMTPSSL {SMTP.sslPort = p}
ok <- case credentials of
(Just (Username u, Password p)) -> SMTP.authenticate SMTP.LOGIN (unpack u) (unpack p) conn
(Just (Username u, Password p)) ->
ensureTimeout $
SMTP.authenticate SMTP.LOGIN (unpack u) (unpack p) conn
_ -> pure True
pure (ok, conn)
create = do
(ok, conn) <- connect
if ok
then Logger.log lg Logger.Debug (msg $ val "Established connection to: " +++ host)
else Logger.log lg Logger.Warn (msg $ val "Failed to established connection, check your credentials to connect to: " +++ host)
pure conn
destroy c = do
SMTP.closeSMTP c
Logger.log lg Logger.Debug (msg $ val "Closing connection to: " +++ host)

sendMail :: MonadIO m => SMTP -> Mail -> m ()
sendMail s m = liftIO $ withResource (s ^. pool) $ SMTP.sendMail m
then pure conn
else CE.throw SMTPUnauthorized

create :: IO SMTP.SMTPConnection
create =
logExceptionOrResult
lg
("Creating pooled SMTP connection to " ++ unpack host)
establishConnection

destroy :: SMTP.SMTPConnection -> IO ()
destroy c =
logExceptionOrResult lg ("Closing pooled SMTP connection to " ++ unpack host) $
(ensureTimeout . SMTP.gracefullyCloseSMTP) c

logExceptionOrResult :: (MonadIO m, MonadCatch m) => Logger -> String -> m a -> m a
logExceptionOrResult lg actionString action = do
res <-
catches
action
[ Handler
( \(e :: SMTPPoolException) -> do
let resultLog = case e of
SMTPUnauthorized ->
("Failed to establish connection, check your credentials." :: String)
SMTPConnectionTimeout -> ("Connection timeout." :: String)
doLog Logger.Warn resultLog
CE.throw e
),
Handler
( \(e :: SomeException) -> do
doLog Logger.Warn ("Caught exception : " ++ show e)
CE.throw e
)
]
doLog Logger.Debug ("Succeeded." :: String)
pure res
where
doLog :: MonadIO m => Logger.Level -> String -> m ()
doLog lvl result =
let msg' = msg ("SMTP connection result" :: String)
in Logger.log lg lvl (msg' . field "action" actionString . field "result" result)

-- | Default timeout for all actions
--
-- It's arguable if this shouldn't become a configuration setting in future.
-- It's an almost obscenely long duration, as we just want to make sure SMTP
-- servers / network components aren't playing tricks to us. Other cases should
Comment thread
supersven marked this conversation as resolved.
Outdated
-- be handled by the network libraries themselves.
defaultTimeoutDuration :: Second
defaultTimeoutDuration = 15

-- | Wrapper function for `SMTP` network actions
--
-- This function ensures that @action@ finishes in a given period of time.
-- Throws on a timeout. Exceptions of @action@ are propagated (re-thrown).
ensureSMTPConnectionTimeout :: (MonadIO m, MonadCatch m, TimeUnit t) => t -> m a -> m a
ensureSMTPConnectionTimeout timeoutDuration action =
timeout timeoutDuration action >>= maybe (CE.throw SMTPConnectionTimeout) pure

-- | Send a `Mail` via an existing `SMTP` connection pool
--
-- Throws exceptions when the SMTP server is unreachable, authentication fails,
-- a timeout happens and on every other network failure.
--
-- `defaultTimeoutDuration` is used as timeout duration for all actions.
sendMail :: (MonadIO m, MonadCatch m) => Logger -> SMTP -> Mail -> m ()
sendMail = sendMail' defaultTimeoutDuration

-- | `sendMail` with configurable timeout duration
--
-- This is mostly useful for testing. (We don't want to waste the amount of
-- `defaultTimeoutDuration` in tests with waiting.)
sendMail' :: (MonadIO m, MonadCatch m, TimeUnit t) => t -> Logger -> SMTP -> Mail -> m ()
sendMail' timeoutDuration lg s m = liftIO $ withResource (s ^. pool) sendMail''
where
sendMail'' :: SMTP.SMTPConnection -> IO ()
sendMail'' c =
logExceptionOrResult lg "Sending mail via SMTP" $
ensureSMTPConnectionTimeout timeoutDuration (SMTP.sendMail m c)
Loading