diff --git a/changelog.d/2-features/smtp-logging b/changelog.d/2-features/smtp-logging deleted file mode 100644 index 496d0aebdd..0000000000 --- a/changelog.d/2-features/smtp-logging +++ /dev/null @@ -1 +0,0 @@ -Add more logs to SMTP mail sending. Ensure that logs are written before the application fails due to SMTP misconfiguration. diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 6c9d92ccb2..b1f22221ba 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -32,7 +32,6 @@ , lens-datetime , lib , mime -, network , optparse-applicative , pem , protobuf @@ -92,7 +91,6 @@ mkDerivation { lens lens-datetime mime - network optparse-applicative pem protobuf diff --git a/libs/types-common/src/Wire/Arbitrary.hs b/libs/types-common/src/Wire/Arbitrary.hs index d5c874263f..ed591cca13 100644 --- a/libs/types-common/src/Wire/Arbitrary.hs +++ b/libs/types-common/src/Wire/Arbitrary.hs @@ -42,10 +42,9 @@ import GHC.Generics (Rep) import Generic.Random (listOf', (:+) ((:+))) import qualified Generic.Random as Generic import Imports -import Network.Socket (PortNumber) import Test.QuickCheck.Arbitrary (Arbitrary (arbitrary)) import qualified Test.QuickCheck.Arbitrary as QC -import Test.QuickCheck.Gen (Gen (MkGen), chooseBoundedIntegral) +import Test.QuickCheck.Gen (Gen (MkGen)) import Test.QuickCheck.Instances () import Test.QuickCheck.Random @@ -121,6 +120,3 @@ generateExample :: Arbitrary a => a generateExample = let (MkGen f) = arbitrary in f (mkQCGen 42) 42 - -instance Arbitrary PortNumber where - arbitrary = chooseBoundedIntegral (minBound :: PortNumber, maxBound :: PortNumber) diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 6a432b2d77..96fb5b5526 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -113,7 +113,6 @@ library , lens >=4.10 , lens-datetime >=0.3 , mime >=0.4.0.2 - , network , optparse-applicative >=0.10 , pem , protobuf >=0.2 diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index ce404aa232..87875e4e3a 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -203,14 +203,6 @@ 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 = { diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 1d5e8c8a44..0cecced516 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -58,7 +58,4 @@ 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); } diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 9c554f38c9..0fd8a01535 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -287,8 +287,6 @@ 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 @@ -465,7 +463,6 @@ executable brig-integration Federation.Util Index.Create Main - SMTP Util Util.AWS @@ -555,16 +552,13 @@ 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 @@ -585,7 +579,6 @@ executable brig-integration , temporary >=1.2.1 , text , time >=1.5 - , time-units , tinylog , transformers , types-common >=0.3 diff --git a/services/brig/default.nix b/services/brig/default.nix index 3a76f885ea..cb9aa3523d 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -85,11 +85,9 @@ , network-conduit-tls , optparse-applicative , pem -, pipes , polysemy , polysemy-plugin , polysemy-wire-zoo -, postie , process , proto-lens , QuickCheck @@ -132,8 +130,6 @@ , text , text-icu-translit , time -, time-out -, time-units , tinylog , transformers , types-common @@ -271,8 +267,6 @@ mkDerivation { text text-icu-translit time - time-out - time-units tinylog transformers types-common @@ -335,16 +329,13 @@ mkDerivation { lens-aeson metrics-wai mime - mime-mail MonadRandom mtl network optparse-applicative pem - pipes polysemy polysemy-wire-zoo - postie process proto-lens QuickCheck @@ -365,7 +356,6 @@ mkDerivation { temporary text time - time-units tinylog transformers types-common diff --git a/services/brig/src/Brig/Email.hs b/services/brig/src/Brig/Email.hs index 26faabe634..00ce2c1ce4 100644 --- a/services/brig/src/Brig/Email.hs +++ b/services/brig/src/Brig/Email.hs @@ -40,20 +40,19 @@ module Brig.Email where import qualified Brig.AWS as AWS -import Brig.App (Env, applog, awsEnv, smtpEnv) +import Brig.App (Env, 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, MonadCatch m, MonadReader Env m) => Mail -> m () +sendMail :: (MonadIO m, MonadReader Env m) => Mail -> m () sendMail m = view smtpEnv >>= \case - Just smtp -> view applog >>= \logger -> SMTP.sendMail logger smtp m + Just smtp -> SMTP.sendMail smtp m Nothing -> view awsEnv >>= \e -> AWS.execute e $ AWS.sendMail m ------------------------------------------------------------------------------- diff --git a/services/brig/src/Brig/SMTP.hs b/services/brig/src/Brig/SMTP.hs index 86bc1a88dc..920752199b 100644 --- a/services/brig/src/Brig/SMTP.hs +++ b/services/brig/src/Brig/SMTP.hs @@ -17,28 +17,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.SMTP - ( sendMail, - initSMTP, - sendMail', - initSMTP', - SMTPConnType (..), - SMTP (..), - Username (..), - Password (..), - SMTPPoolException (..), - ) -where +module Brig.SMTP 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 @@ -65,73 +50,17 @@ deriveJSON defaultOptions {constructorTagModifier = map toLower} ''SMTPConnType makeLenses ''SMTP -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. --- --- `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 - ) +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" SMTP <$> createPool create destroy 1 5 5 where - ensureTimeout :: IO a -> IO a - ensureTimeout = ensureSMTPConnectionTimeout timeoutDuration - - establishConnection :: IO SMTP.SMTPConnection - establishConnection = do - conn <- ensureTimeout $ case (connType, port) of + connect = do + conn <- case (connType, port) of (Plain, Nothing) -> SMTP.connectSMTP (unpack host) (Plain, Just p) -> SMTP.connectSMTPPort (unpack host) p (TLS, Nothing) -> SMTP.connectSMTPSTARTTLS (unpack host) @@ -143,88 +72,18 @@ initSMTP' timeoutDuration lg host port credentials connType = do SMTP.connectSMTPSSLWithSettings (unpack host) $ SMTP.defaultSettingsSMTPSSL {SMTP.sslPort = p} ok <- case credentials of - (Just (Username u, Password p)) -> - ensureTimeout $ - SMTP.authenticate SMTP.LOGIN (unpack u) (unpack p) conn + (Just (Username u, Password p)) -> SMTP.authenticate SMTP.LOGIN (unpack u) (unpack p) conn _ -> pure True + pure (ok, conn) + create = do + (ok, conn) <- connect if ok - 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 --- 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) + 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 diff --git a/services/brig/src/Brig/User/Email.hs b/services/brig/src/Brig/User/Email.hs index 5d200af829..73063a10c9 100644 --- a/services/brig/src/Brig/User/Email.hs +++ b/services/brig/src/Brig/User/Email.hs @@ -42,7 +42,6 @@ import Brig.Types.Activation (ActivationPair) import Brig.Types.User (PasswordResetPair) import Brig.User.Template import Control.Lens (view) -import Control.Monad.Catch import qualified Data.Code as Code import Data.Json.Util (fromUTCTimeMillis) import Data.Range @@ -56,7 +55,6 @@ import Wire.API.User.Password sendVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -71,8 +69,7 @@ sendVerificationMail to pair loc = do sendLoginVerificationMail :: ( MonadReader Env m, - MonadIO m, - MonadCatch m + MonadIO m ) => Email -> Code.Value -> @@ -85,7 +82,6 @@ sendLoginVerificationMail email code mbLocale = do sendCreateScimTokenVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -99,7 +95,6 @@ sendCreateScimTokenVerificationMail email code mbLocale = do sendTeamDeletionVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -113,7 +108,6 @@ sendTeamDeletionVerificationMail email code mbLocale = do sendActivationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -135,7 +129,6 @@ sendActivationMail to name pair loc ident = do sendPasswordResetMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -150,7 +143,6 @@ sendPasswordResetMail to pair loc = do sendDeletionEmail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Name -> @@ -166,7 +158,6 @@ sendDeletionEmail name email key code locale = do sendNewClientEmail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Name -> @@ -181,7 +172,6 @@ sendNewClientEmail name email client locale = do sendTeamActivationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index dea31d0ff6..7fab864c0a 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -54,7 +54,6 @@ import Network.HTTP.Client.TLS (tlsManagerSettings) import Network.Wai.Utilities.Server (compile) import OpenSSL (withOpenSSL) import Options.Applicative hiding (action) -import qualified SMTP import System.Environment (withArgs) import qualified System.Environment.Blank as Blank import qualified System.Logger as Logger @@ -152,9 +151,9 @@ runTests iConf brigOpts otherArgs = do includeFederationTests <- (== Just "1") <$> Blank.getEnv "INTEGRATION_FEDERATION_TESTS" internalApi <- API.Internal.tests brigOpts mg db b (brig iConf) gd g - let smtp = SMTP.tests mg lg - versionApi = API.Version.tests mg brigOpts b - mlsApi = MLS.tests mg b brigOpts + let versionApi = API.Version.tests mg brigOpts b + + let mlsApi = MLS.tests mg b brigOpts withArgs otherArgs . defaultMain $ testGroup @@ -177,8 +176,7 @@ runTests iConf brigOpts otherArgs = do federationEndpoints, internalApi, versionApi, - mlsApi, - smtp + mlsApi ] <> [federationEnd2End | includeFederationTests] where diff --git a/services/brig/test/integration/SMTP.hs b/services/brig/test/integration/SMTP.hs deleted file mode 100644 index f565b3078e..0000000000 --- a/services/brig/test/integration/SMTP.hs +++ /dev/null @@ -1,256 +0,0 @@ -module SMTP where - -import qualified Bilge -import Brig.SMTP -import Control.Exception -import qualified Data.ByteString as B -import qualified Data.ByteString.Char8 as C -import Data.Text (unpack) -import Data.Text.Lazy (fromStrict) -import Data.Time.Units -import Foreign.C.Error (Errno (..), eCONNREFUSED) -import GHC.IO.Exception (ioe_errno) -import Imports -import Network.Mail.Mime -import qualified Network.Mail.Postie as Postie -import Network.Socket -import qualified Pipes.Prelude -import qualified System.Logger as Logger -import Test.QuickCheck -import Test.Tasty -import Test.Tasty.HUnit -import Util - -tests :: Bilge.Manager -> Logger.Logger -> TestTree -tests m lg = - testGroup - "SMTP" - [ test m "should send mail" $ testSendMail lg, - test m "should throw exception when SMTP server refuses to send mail (mail without receiver)" $ testSendMailNoReceiver lg, - test m "should throw when an SMTP transaction is aborted (SMTP error 554: 'Transaction failed')" $ testSendMailTransactionFailed lg, - test m "should throw an error when the connection cannot be initiated on startup" $ testSendMailFailingConnectionOnStartup lg, - test m "should throw when the server cannot be reached on sending" $ testSendMailFailingConnectionOnSend lg, - test m "should throw when sending times out" $ testSendMailTimeout lg, - test m "should throw an error the initiation times out" $ testSendMailTimeoutOnStartup lg - ] - -testSendMail :: Logger.Logger -> Bilge.Http () -testSendMail lg = do - port <- randomPortNumber - receivedMailRef <- liftIO $ newIORef Nothing - liftIO - . withMailServer port (mailStoringApp receivedMailRef) - $ do - conPool <- initSMTP lg "localhost" (Just port) Nothing Plain - sendMail lg conPool someTestMail - mbMail <- - retryWhileN 3 isJust $ do - readIORef receivedMailRef - isJust mbMail @? "Expected to receive mail" - postieAddressAsString . rmSender <$> mbMail - @=? (Just . unpack . addressEmail) someTestSender - postieAddressAsString <$> (concat . maybeToList) (rmReceipients <$> mbMail) - @=? [(unpack . addressEmail) someTestReceiver] - let mailContent = (rmContent . fromJust) mbMail - elem (unpack someTestBody) mailContent @? "Expected the SMTP server to receive the mail body." - elem ("Subject: " ++ unpack someTestSubject) mailContent @? "Expected the SMTP server to receive the mail subject." - where - postieAddressAsString :: Postie.Address -> String - postieAddressAsString addr = - toString - ( B.concat - [ Postie.addressLocalPart addr, - C.singleton '@', - Postie.addressDomain addr - ] - ) - -testSendMailNoReceiver :: Logger.Logger -> Bilge.Http () -testSendMailNoReceiver lg = do - port <- randomPortNumber - receivedMailRef <- liftIO $ newIORef Nothing - liftIO - . withMailServer port (mailStoringApp receivedMailRef) - $ do - conPool <- initSMTP lg "localhost" (Just port) Nothing Plain - caughtException <- - handle @SomeException - (const (pure True)) - (sendMail lg conPool (emptyMail (Address Nothing "foo@example.com")) >> pure False) - caughtException @? "Expected exception due to missing mail receiver." - -testSendMailTransactionFailed :: Logger.Logger -> Bilge.Http () -testSendMailTransactionFailed lg = do - port <- randomPortNumber - liftIO - . withMailServer port mailRejectingApp - $ do - conPool <- initSMTP lg "localhost" (Just port) Nothing Plain - caughtException <- - handle @SomeException - (const (pure True)) - (sendMail lg conPool someTestMail >> pure False) - caughtException @? "Expected exception due to missing mail receiver." - -testSendMailFailingConnectionOnStartup :: Logger.Logger -> Bilge.Http () -testSendMailFailingConnectionOnStartup lg = do - port <- randomPortNumber - caughtError <- - liftIO $ - handle @ErrorCall - (const (pure True)) - (initSMTP lg "localhost" (Just port) Nothing Plain >> pure False) - liftIO $ caughtError @? "Expected error (SMTP server unreachable.)" - -testSendMailFailingConnectionOnSend :: Logger.Logger -> Bilge.Http () -testSendMailFailingConnectionOnSend lg = do - port <- randomPortNumber - receivedMailRef <- liftIO $ newIORef Nothing - conPool <- - liftIO $ - withMailServer - port - (mailStoringApp receivedMailRef) - (initSMTP lg "localhost" (Just port) Nothing Plain) - caughtException <- - liftIO $ - handle @SomeException - (const (pure True)) - (sendMail lg conPool someTestMail >> pure False) - liftIO $ caughtException @? "Expected exception (SMTP server unreachable.)" - mbMail <- liftIO $ readIORef receivedMailRef - liftIO $ isNothing mbMail @? "No mail expected (if there is one, the test setup is broken.)" - -testSendMailTimeout :: Logger.Logger -> Bilge.Http () -testSendMailTimeout lg = do - port <- randomPortNumber - mbException <- - liftIO $ - withMailServer port (delayingApp (3 :: Second)) $ - do - conPool <- initSMTP lg "localhost" (Just port) Nothing Plain - handle @SMTPPoolException - (\e -> pure (Just e)) - (sendMail' (500 :: Millisecond) lg conPool someTestMail >> pure Nothing) - liftIO $ isJust mbException @? "Expected exception (SMTP server action timed out.)" - liftIO $ mbException @?= Just SMTPConnectionTimeout - -testSendMailTimeoutOnStartup :: Logger.Logger -> Bilge.Http () -testSendMailTimeoutOnStartup lg = do - port <- randomPortNumber - mbException <- - liftIO $ - everDelayingTCPServer port $ - handle @ErrorCall - (\e -> pure (Just e)) - (initSMTP' (500 :: Millisecond) lg "localhost" (Just port) Nothing Plain >> pure Nothing) - liftIO $ isJust mbException @? "Expected exception (SMTP server action timed out.)" - -someTestReceiver :: Address -someTestReceiver = Address Nothing "foo@example.com" - -someTestSender :: Address -someTestSender = Address Nothing "bar@example.com" - -someTestSubject :: Text -someTestSubject = "Some Subject" - -someTestBody :: Text -someTestBody = "Some body" - -someTestMail :: Mail -someTestMail = - simpleMail' - someTestReceiver - someTestSender - someTestSubject - (fromStrict someTestBody) - -toString :: B.ByteString -> String -toString bs = C.foldr (:) [] bs - -withMailServer :: PortNumber -> Postie.Application -> IO a -> IO a -withMailServer port app action = do - bracket - (forkIO $ Postie.run (portNumberToInt port) app) - killThread - (const action) - where - portNumberToInt = fromInteger . toInteger - -data ReceivedMail = ReceivedMail - { rmSender :: Postie.Address, - rmReceipients :: [Postie.Address], - -- | Contains all data sent to the SMTP server for this mail. (Including - -- /From:/, /To:/, /Subject:/, ... lines.) I.e. `Postie.mailBody` is half of - -- a lie; it's way more. - rmContent :: [String] - } - deriving (Eq, Show) - -mailStoringApp :: IORef (Maybe ReceivedMail) -> Postie.Application -mailStoringApp receivedMailRef mail = do - c <- Pipes.Prelude.toListM (Postie.mailBody mail) - let receivedMail = - ReceivedMail - { rmSender = Postie.mailSender mail, - rmReceipients = Postie.mailRecipients mail, - rmContent = C.unpack <$> c - } - writeIORef receivedMailRef (Just receivedMail) - pure Postie.Accepted - -mailRejectingApp :: Postie.Application -mailRejectingApp = const (pure Postie.Rejected) - -mailAcceptingApp :: Postie.Application -mailAcceptingApp = const (pure Postie.Accepted) - -delayingApp :: (TimeUnit t) => t -> Postie.Application -delayingApp delay = - const - ( (threadDelay . fromInteger . toMicroseconds) delay - >> pure Postie.Accepted - ) - -everDelayingTCPServer :: PortNumber -> IO a -> IO a -everDelayingTCPServer port action = withSocketsDo $ do - addr <- resolve - bracket (open addr) close (const action) - where - portString :: String - portString = (show . toInteger) port - resolve = do - let hints = - defaultHints - { addrFlags = [AI_PASSIVE], - addrSocketType = Stream - } - head <$> getAddrInfo (Just hints) Nothing (Just portString) - open addr = bracketOnError (openSocket addr) close $ \sock -> do - setSocketOption sock ReuseAddr 1 - withFdSocket sock setCloseOnExecIfNeeded - bind sock $ addrAddress addr - listen sock 1024 - pure sock - -randomPortNumber :: MonadIO m => m PortNumber -randomPortNumber = do - candidate <- liftIO $ generate (arbitrary `suchThat` (> 1024)) - portOpen <- liftIO $ isPortOpen candidate - if portOpen - then randomPortNumber - else pure candidate - -isPortOpen :: PortNumber -> IO Bool -isPortOpen port = do - let sockAddr = SockAddrInet port (tupleToHostAddress (127, 0, 0, 1)) - tcpProtocolNumber = 6 - bracket (socket AF_INET Stream tcpProtocolNumber) close' $ \sock -> do - res <- try $ connect sock sockAddr - case res of - Right () -> pure True - Left e -> - if (Errno <$> ioe_errno e) == Just eCONNREFUSED - then pure False - else throwIO e