assumpta (empty) → 0.1.0.0
raw patch · 19 files changed
+1541/−0 lines, 19 filesdep +QuickCheckdep +assumptadep +assumpta-coresetup-changed
Dependencies added: QuickCheck, assumpta, assumpta-core, base, bytestring, connection, data-default, exceptions, filepath, hspec, mime-mail, mtl, network, quickcheck-io, shelly, text, transformers
Files
- CONTRIBUTE.md +24/−0
- ChangeLog.md +7/−0
- DEVELOPMENT.md +39/−0
- LICENSE +24/−0
- README.md +13/−0
- Setup.hs +2/−0
- TODO.md +27/−0
- assumpta.cabal +139/−0
- examples/simple-client-session-bs.hs +48/−0
- examples/simple-client-session-text.hs +46/−0
- scripts/stack-test.sh +12/−0
- src/Network/Mail/Assumpta/ByteString.hs +309/−0
- src/Network/Mail/Assumpta/Internal/Net.hs +130/−0
- src/Network/Mail/Assumpta/String.hs +159/−0
- src/Network/Mail/Assumpta/Text.hs +299/−0
- stack-lts-12.26.yaml +7/−0
- test-hspec/Network/Mail/Assumpta/ByteStringSpec.hs +175/−0
- test-hspec/hspec-tests.hs +1/−0
- test-other/compile-examples.hs +80/−0
+ CONTRIBUTE.md view
@@ -0,0 +1,24 @@++Patches/pull requests welcome,+follow the usual `git` workflow for submitting a pull request:++- fork the project+- create a new branch from master (e.g. `features/my-new-feature` or+ `issue/123-my-bugfix`)++If you're fixing a bug also create an issue +on [GitHub](https://github.com/phlummox/assumpta/issues)+if one doesn't exist yet.++If it's a new feature explain why you think it's useful or necessary.++Follow the same coding style with regards to spaces, semicolons, variable+naming etc. In general, we follow the usual Haskell naming convention of making+function and variable names `camelCase`+(even+when they include+[acronyms](https://stackoverflow.com/questions/15526107/acronyms-in-camelcase)).+Mostly.++Please add tests for any contributions you make.+
+ ChangeLog.md view
@@ -0,0 +1,7 @@+# Changelog for hs-smtp-client++## Unreleased changes++## 0.1.0.0++Unleashed on the world.
+ DEVELOPMENT.md view
@@ -0,0 +1,39 @@+# Development notes and tips++I build and test assumpta and assumpta-core+using [`stack`](https://docs.haskellstack.org/en/stable/README/),+plus CI services (Travis CI and CircleCI).++CircleCI in general is much more irritating, doesn't natively support+'test matrixes', and has obviously had less thought put into its design;+but it's faster, seems to do a better job of cacheing build artifacts,+and lets you use any Docker image you like to run tests in (unlike Travis CI,+which only has a fixed set of supported environments).++If anyone wants to suggest things which might make the source tree+more friendly to cabal-users, feel free to let me know.++## Tests++### Compile-examples++The test suite `compile-examples` (which checks that the+programs in the `examples` directory can be compiled)+requires stack to run, and will+fail if a `STACK_RESOLVER` environment variable isn't found (or if+it can't find `stack` on the `$PATH`). It also is normally disabled,+unless the `stack-based-tests` flag is enabled.++So it is best run like this:++```bash+$ export STACK_RESOLVER=lts-12.26+$ stack --resolver="${STACK_RESOLVER}" test --flag assumpta:stack-based-tests+```++The aim of the test is just to ensure that the examples+are in sync with the API, so I haven't attempted to+get the test working in any other environment.+++
+ LICENSE view
@@ -0,0 +1,24 @@+Copyright 2019 Phlummox++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,13 @@+# assumpta [](https://hackage.haskell.org/package/assumpta) [](https://travis-ci.com/phlummox/assumpta) [](https://circleci.com/gh/phlummox/assumpta)++An SMTP client library. It provides functionality for sending SMTP+commands (including `STARTTLS`) to a server and checking for expected+responses. Also for just sending an email.++## Usage++See the `simple-client-session-bs.hs` program in the `examples`+directory, or the `Network.Mail.Assumpta.ByteString` module+for examples of usage.++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ TODO.md view
@@ -0,0 +1,27 @@++# TODO++## Add 'period-escaping' functions for ByteStrings++The Text and String modules have utility functions for+escaping periods at the start of lines, the+ByteString one does not, as yet.+The 'stringsearch' package will do search-and-replace on+ByteStrings if we don't want to write our own impl.++## easy ways of mocking and logging++- Nice simple ergonomic way of substituting a mock+ server for a real one, and for logging data passing+ thru++## more tests++- Test that package builds cleanly from an sdist+ source distribution tgz.+- Test that Haddock documentation builds cleanly++## build sdist from CI++- add Releases to GitHub+
+ assumpta.cabal view
@@ -0,0 +1,139 @@+cabal-version: 1.12++name: assumpta+version: 0.1.0.0+synopsis: An SMTP client library+description: An SMTP client library,+ which allows you to send email via+ an SMTP server.+ .+ For further details, please see the README on GitHub at+ <https://github.com/phlummox/assumpta#readme>.+category: Network+homepage: https://github.com/phlummox/assumpta#readme+bug-reports: https://github.com/phlummox/assumpta/issues+author: phlummox+maintainer: phlummox2@gmail.com+copyright: 2020 phlummox+license: BSD2+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md+ CONTRIBUTE.md+ DEVELOPMENT.md+ TODO.md+ examples/simple-client-session-bs.hs+ examples/simple-client-session-text.hs+ scripts/stack-test.sh+ stack-lts-12.26.yaml++source-repository head+ type: git+ location: https://github.com/phlummox/assumpta++-- This enables (or disables) tests that rely on+-- "stack": just the example compilation+-- test, at the moment.+-- You can enable these tests from stack using+-- `--flag assumpta-core:stack-based-tests`+flag stack-based-tests+ description: enable tests requiring stack+ manual: True+ default: False++flag warnmore+ description: Enable plenty of ghc warning flags+ manual: True+ default: True++library+ exposed-modules:+ Network.Mail.Assumpta.Internal.Net+ Network.Mail.Assumpta.ByteString+ Network.Mail.Assumpta.Text+ Network.Mail.Assumpta.String+ hs-source-dirs:+ src+ build-depends:+ base >=4.0 && <5+ , assumpta-core == 0.1.*+ , bytestring+ , connection+ , data-default+ , exceptions+ , mime-mail+ , mtl+ , text+ , transformers+ if flag(warnmore)+ if impl(ghc >= 8.0.1)+ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints+ -Wno-name-shadowing -Wno-orphans -fwarn-tabs+ else+ ghc-options:+ -Wall+ default-language: Haskell2010++++test-suite hspec-tests+ type: exitcode-stdio-1.0+ main-is: hspec-tests.hs+ other-modules:+ Network.Mail.Assumpta.ByteStringSpec+ hs-source-dirs:+ test-hspec+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ QuickCheck+ , assumpta+ , base >=4.0 && <5+ , assumpta-core+ , bytestring+ , hspec+ , network < 3+ , quickcheck-io+ if flag(warnmore)+ if impl(ghc >= 8.0.1)+ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints+ -Wno-name-shadowing -Wno-orphans -fwarn-tabs+ else+ ghc-options:+ -Wall+ default-language: Haskell2010++-- **+-- - Only works with stack+-- - requires STACK_RESOLVER env var to be set+test-suite compile-examples+ type: exitcode-stdio-1.0+ main-is: compile-examples.hs+ hs-source-dirs:+ test-other+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ if !(flag(stack-based-tests))+ buildable: False+ else+ build-depends:+ assumpta+ , assumpta-core+ , base >=4.0 && <5+ , filepath+ , shelly+ , text+ if flag(warnmore)+ if impl(ghc >= 8.0.1)+ ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints+ -Wno-name-shadowing -Wno-orphans -fwarn-tabs+ else+ ghc-options:+ -Wall+ default-language: Haskell2010
+ examples/simple-client-session-bs.hs view
@@ -0,0 +1,48 @@++{-# LANGUAGE OverloadedStrings #-}++-- Program that runs a short SMTP session+-- with a server running on local port 2025.++module Main where++import Data.Monoid+import qualified Data.ByteString.Char8 as BS+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Network.BSD (getHostName) -- needs 'network' package for this+import Network.Mail.Assumpta.ByteString as M++sampleMesg :: BS.ByteString+sampleMesg = BS.intercalate crlf [+ "Date: Tue, 21 Jan 2020 02:28:37 +0800"+ , "To: neddie.seagoon@gmail.com"+ , "From: jim.moriarty@gmail.com"+ , "Subject: test Tue, 21 Jan 2020 02:28:37 +0800"+ , ""+ , "Sapristi nyuckoes!"+ ]++-- The sender and recipient supplied to the+-- SMTP server are what is used to route the+-- message; any 'To:' and 'From:' fields+-- in the message are completely ignored for this+-- purpose.+sender, recipient :: BS.ByteString+sender = "somesender@mycorp.com"+recipient = "somerecipient@mozilla.org"++main :: IO ()+main = do+ let toBinary = TE.encodeUtf8 . T.pack + port = 2025+ hostname <- getHostName+ print =<< runSmtp "localhost" port (do+ expectGreeting+ ehlo $ toBinary hostname+ -- Properly, we should escape periods at the start of a+ -- line. But we know there aren't any+ sendRawMail sender [recipient] sampleMesg+ quit)++
+ examples/simple-client-session-text.hs view
@@ -0,0 +1,46 @@++{-# LANGUAGE OverloadedStrings #-}++-- Program that runs a short SMTP session+-- with a server running on local port 2025.++module Main where++import Data.Monoid+import qualified Data.Text as T+import Network.BSD (getHostName) -- requires 'network' package+import Network.Mail.Assumpta.Text++sampleMesg :: T.Text+sampleMesg = T.intercalate crlf [+ "Date: Tue, 21 Jan 2020 02:28:37 +0800"+ , "To: neddie.seagoon@gmail.com"+ , "From: jim.moriarty@gmail.com"+ , "Subject: test Tue, 21 Jan 2020 02:28:37 +0800"+ , ""+ , "Sapristi nyuckoes!"+ ]++-- The sender and recipient supplied to the+-- SMTP server are what is used to route the+-- message; any 'To:' and 'From:' fields+-- in the message are completely ignored for this+-- purpose.+sender, recipient :: T.Text+sender = "somesender@mycorp.com"+recipient = "somerecipient@mozilla.org"++main :: IO ()+main = do+ let + port = 2025+ hostname <- getHostName+ print =<< runSmtp "localhost" port (do+ expectGreeting+ ehlo $ T.pack hostname+ -- Properly, we should escape periods at the start of a+ -- line. But we know there aren't any+ sendRawMail sender [recipient] sampleMesg+ quit)++
+ scripts/stack-test.sh view
@@ -0,0 +1,12 @@+#!/usr/bin/env bash++# run 'stack-only' tests with some resolver++if [ "$#" -ne 1 ]; then+ echo "expected one arg, a resolver to use (e.g. 'lts-11')"+ exit 1+fi++export STACK_RESOLVER="$1"+stack --resolver="${STACK_RESOLVER}" test --flag assumpta:stack-based-tests+
+ src/Network/Mail/Assumpta/ByteString.hs view
@@ -0,0 +1,309 @@++{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}++#if __GLASGOW_HASKELL__ >= 801+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+#endif++{-# OPTIONS_HADDOCK show-extensions #-}++{- |++Communicate with SMTP servers using raw 'ByteString's, using+the infrastructure of the+<https://hackage.haskell.org/package/connection connection>+package.++This module does not validate that your bytestrings+satisfy the requirements of the relevant RFCs -- see the note in+"Network.Mail.Assumpta.MonadSmtp#permissiblecharacters"+about "permissible characters". In general, unless you are using+an SMTP extension, your bytestrings must be 7-bit clean ASCII.++== Example code++Short example (which will work if you have an SMTP server+running on local port 2025):+(see the+<https://hackage.haskell.org/package/assumpta/src/examples/ examples>+directory for a copy of the code):++> {-# LANGUAGE OverloadedStrings #-}+> +> -- Program that runs a short SMTP session+> -- with a server running on local port 2025.+> +> module Main where+> +> import Data.Monoid+> import qualified Data.ByteString.Char8 as BS+> import qualified Data.Text as T+> import qualified Data.Text.Encoding as TE+> import Network.BSD (getHostName) -- requires 'network' package+> import Network.Mail.Assumpta.ByteString as M+> +> sampleMesg :: BS.ByteString+> sampleMesg = BS.intercalate crlf [+> "Date: Tue, 21 Jan 2020 02:28:37 +0800"+> , "To: neddie.seagoon@gmail.com"+> , "From: jim.moriarty@gmail.com"+> , "Subject: test Tue, 21 Jan 2020 02:28:37 +0800"+> , ""+> , "Sapristi nyuckoes!"+> ]+> +> -- The sender and recipient supplied to the+> -- SMTP server are what is used to route the+> -- message; any 'To:' and 'From:' fields+> -- in the message are completely ignored for this+> -- purpose.+> sender, recipient :: BS.ByteString+> sender = "somesender@mycorp.com"+> recipient = "somerecipient@mozilla.org"+> +> main :: IO ()+> main = do+> let toBinary = TE.encodeUtf8 . T.pack +> port = 2025+> hostname <- getHostName+> print =<< runSmtp "localhost" port (do+> expectGreeting+> ehlo $ toBinary hostname+> -- Properly, we should escape periods at the start of a+> -- line. But we know there aren't any+> sendRawMail sender [recipient] sampleMesg+> quit)++== Alternatives to "connection"++If you want to use other network libraries besides+<https://hackage.haskell.org/package/connection connection>,+it should be pretty straightforward to adapt the code here.++If you want to use stream-like IO, one possibility is+the <https://hackage.haskell.org/package/conduit-connection conduit-connection> package,+which provides Conduit-based sources and sinks on top of "Network.Connection".+++-}++module Network.Mail.Assumpta.ByteString+ (+ -- * Run Smtp actions+ -- | Run actions in the 'MonadSmtp' monad.+ runSmtp+ , runSecureSmtp+ , runSmtpWithParams+ , runSmtpHandle+ -- * MonadError variants+ -- | Instead of returning an Either, these run in MonadError --+ -- thus the caller can specialize them to a 'Maybe', 'Either',+ -- or some other 'MonadError' instance as desired.+ , runSmtp'+ , runSecureSmtp'+ , runSmtpWithParams'+ , runSmtpHandle'+ -- * SMTP commands+ -- | Functions for sending commands+ -- to an SMTP server. In general, these are wrappers around+ -- 'command' and 'expect' -- they send some command,+ -- and try to parse a response (throwing an 'SmtpError'+ -- on failure).+ , helo+ , ehlo+ , mailFrom+ , rcptTo+ , data_+ , noop+ , quit+ , rset+ , startTLS+ , expn+ , vrfy+ , help+ , Smtp.sendRawMail+ -- * Server responses+ , expect+ , expectGreeting+ -- * Low-level operations and types+ , sendLine+ , command+ , Reply+ , ReplyLine(..)+ , ReplyCode+ , SmtpError(..)+ -- * Monad transformer+ -- | A monad transformer, 'SmtpT', which provides the ability to+ -- send SMTP commands and parse replies, together with operations+ -- on the transformer.+ , SmtpT+ , Smtp+ , liftSmtpT+ , mapSmtpT+ -- * Network operations+ -- | Network operations based around 'Handle',+ -- using the+ -- <https://hackage.haskell.org/package/connection connection>+ -- package.+ , open+ , openTls+ , openParams+ , close+ , withHandle+ , withSecureHandle+ , withHandleParams+ , HostName+ , Port+ , Handle(..)+ , defaultTLSSettings+ -- * Utility functions+ , rethrow+ , mkParams+ , toIOError+ , crlf+ )+ where++import Control.Monad.Catch (MonadMask)+import Control.Monad.Except+import qualified Network.Connection as C++import Network.Mail.Assumpta.Internal.Net+import Network.Mail.Assumpta.MonadSmtp as Smtp+import qualified Network.Mail.Assumpta.Trans.Smtp as T+import Network.Mail.Assumpta.Trans.Smtp hiding ( SmtpT(..)+ , runSmtpEither+ , rethrow+ , runSmtp, open+ , close )+++-- --+-- Specialization of monad transformer to Handle++-- | Smtp monad transformer. A specialization to 'Handle'+-- of the more general SmtpT type in the+-- <http://hackage.haskell.org/package/assumpta-core assumpta-core>+-- package.+type SmtpT = T.SmtpT Handle++-- | Smtp actions in the IO monad.+type Smtp = SmtpT IO++-- base funcs++-- | @runSmtpHandle h a@+-- +-- Run some 'Smtp' action @a@ on a 'Handle' @h@,+-- and return the result as an 'Either'.+runSmtpHandle ::+ Handle -> Smtp a -> IO (Either SmtpError a)+runSmtpHandle = T.runSmtpEither++-- | @runSmtpHandle h a@+-- +-- Run some 'Smtp' action @a@ on a 'Handle' @h@,+-- and return the result as a 'MonadError'.+--+-- Like 'runSmtpHandle' but generalized to 'MonadError'.+runSmtpHandle' ::+ (MonadError SmtpError m, MonadIO m) =>+ Handle -> SmtpT m a -> m a+runSmtpHandle' = T.runSmtp+++++-- | @runSmtp hostname port a@+--+-- Open a connection to the specified @hostname@ and @port@,+-- run some 'Smtp' action @a@ with it, then+-- close, returning the result as an 'Either'.+runSmtp ::+ HostName -> Int -> Smtp a -> IO (Either SmtpError a)+runSmtp host port a =+ withHandle host port (`runSmtpHandle` a)++-- | @runSmtp' host port a@+--+-- Like 'runSmtp', but generalized to 'MonadError'.+runSmtp' ::+ (MonadMask m, MonadIO m, MonadError SmtpError m) =>+ HostName -> Port -> SmtpT m b -> m b+runSmtp' host port a =+ withHandle host port (`runSmtpHandle'` a)++-- | @runSecureSmtp hostname port a@+--+-- Open a secure TLS connection to the specified @hostname@ and @port@,+-- run some 'Smtp' action @a@ with it, then+-- close, returning the result as an 'Either'.+--+-- Uses the default TLS settings, 'defaultTLSSettings'. For+-- more control, use 'runSmtpWithParams'.+runSecureSmtp ::+ HostName -> Int -> Smtp a -> IO (Either SmtpError a)+runSecureSmtp host port a = + let params = (mkParams host port) {+ C.connectionUseSecure = Just defaultTLSSettings+ }+ in runSmtpWithParams params a++-- | @runSecureSmtp' host port a@+--+-- Like 'runSecureSmtp', but generalized to 'MonadError'.+runSecureSmtp' ::+ (MonadMask m, MonadIO m, MonadError SmtpError m) =>+ HostName -> Int -> SmtpT m b -> m b+runSecureSmtp' host port a = + let params = (mkParams host port) {+ C.connectionUseSecure = Just defaultTLSSettings+ }+ in runSmtpWithParams' params a+++-- | @runSmtpWithParams params a@+--+-- Like 'runSmtp', but providing more control --+-- the actions are run using the specified connection +-- parameters (hostname, port, TLS settings, etc.).+runSmtpWithParams ::+ C.ConnectionParams -> Smtp a -> IO (Either SmtpError a)+runSmtpWithParams p a =+ withHandleParams p (`runSmtpHandle` a)+++-- | @runSmtpWithParams' params a@+--+-- Like 'runSmtpWithParams', but generalized to 'MonadError'.+runSmtpWithParams' ::+ (MonadMask m, MonadIO m, MonadError SmtpError m) =>+ C.ConnectionParams -> SmtpT m b -> m b+runSmtpWithParams' p a =+ withHandleParams p (`runSmtpHandle'` a)++-- -- | @runSmtpT h a@+-- +-- Given some 'Handle' @h@ and an 'SmtpT' action @a@,+-- run the action+-- and return the result as an 'Either'.+--runSmtpT' ::+-- MonadIO m =>+-- Handle -> (forall n . Ass.MonadSmtp n => n a) -> m (Either SmtpError a)+--runSmtpT' = T.runSmtpEither+++-- | Convenience function: convert an 'SmtpError' into +-- an 'IOError'.+toIOError :: SmtpError -> IOError+toIOError ex = userError $ "smtp operation error: " ++ show ex++-- | convenience function: re-throw an 'SmtpError' as an+-- exception in 'IO'.+rethrow :: Either SmtpError a -> IO a+rethrow =+ either (ioError . toIOError) return++
+ src/Network/Mail/Assumpta/Internal/Net.hs view
@@ -0,0 +1,130 @@++{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}++{- |++Network operations using+the <https://hackage.haskell.org/package/connection connection>+package.++-}++module Network.Mail.Assumpta.Internal.Net++ where++import Control.Monad.Catch (MonadMask)+import Control.Monad.IO.Class+import Data.Default (def)+import qualified Network.Connection as NC++import Network.Mail.Assumpta.Connection as C++-- --+-- Network types and ops++type Port = Int+type HostName = String++-- | Network handle, containing enough+-- information to both communicate a connection,+-- and upgrade to TLS.+data Handle = Handle {+ hConn :: !NC.Connection+ , hContext :: !NC.ConnectionContext+ }++-- Implementation of 'Connection' using+-- the <https://hackage.haskell.org/package/connection connection>+-- package.+instance Connection Handle where+ type Cstrt Handle = MonadIO+ type Params Handle = NC.ConnectionParams ++ open params = do ctx <- liftIO NC.initConnectionContext+ conn <- liftIO $ NC.connectTo ctx params+ return $ Handle conn ctx+ close = liftIO . NC.connectionClose . hConn++ send h bs = liftIO $ NC.connectionPut (hConn h) bs+ recv h = liftIO $ NC.connectionGet (hConn h) 2048++ upgrade h = let conn = hConn h+ ctx = hContext h+ tls = defaultTLSSettings+ in liftIO $ NC.connectionSetSecure ctx conn tls++-- | default TLS settings+defaultTLSSettings :: NC.TLSSettings+defaultTLSSettings = def++mkParams ::+ HostName -> Int -> NC.ConnectionParams+mkParams hostname port =+ NC.ConnectionParams {+ NC.connectionHostname = hostname+ , NC.connectionPort = fromIntegral port+ , NC.connectionUseSecure = Nothing+ , NC.connectionUseSocks = Nothing+ }++-- | Open a network 'Handle' with the specified 'NC.ConnectionParams'+openParams ::+ MonadIO m => NC.ConnectionParams -> m Handle+openParams =+ C.open++-- | Open a network 'Handle' to the specified hostname and port+open :: MonadIO m => HostName -> Port -> m Handle+open host port =+ let p = mkParams host port+ in openParams p+++-- | Open a secure network 'Handle' to the specified hostname and port+-- using the default TLS settings ('defaultTLSSettings')+openTls :: MonadIO m => HostName -> Port -> m Handle+openTls host port =+ let p = (mkParams host port) { NC.connectionUseSecure = Just defaultTLSSettings }+ in openParams p++-- | Close a network 'Handle'+close :: MonadIO m => Handle -> m ()+close =+ C.close++-- | @withHandleParams p a@+--+-- Given some parameters @p@ (hostname, port etc) for opening a 'Handle': +-- open a handle, run some action @a@ with it, then+-- close.+withHandleParams ::+ (MonadMask m, MonadIO m) => NC.ConnectionParams -> (Handle -> m b) -> m b+withHandleParams =+ C.withConnection++-- | @withHandle hostname port a@+--+-- Open a 'Handle' to the specified @hostname@ and @port@,+-- run some action @a@ with it, then+-- close.+withHandle ::+ (MonadMask m, MonadIO m) => HostName -> Port -> (Handle -> m b) -> m b+withHandle host port =+ let p = mkParams host port+ in withHandleParams p++-- | @withSecureHandle hostname port a@+--+-- Open a secure 'Handle' to the specified @hostname@ and @port@,+-- run some action @a@ with it, then+-- close.+withSecureHandle ::+ (MonadMask m, MonadIO m) => HostName -> Port -> (Handle -> m b) -> m b+withSecureHandle host port =+ let p = (mkParams host port) { + NC.connectionUseSecure = Just defaultTLSSettings+ }+ in withHandleParams p+
+ src/Network/Mail/Assumpta/String.hs view
@@ -0,0 +1,159 @@++{- |++'String' versions of Smtp operations.++This module does not validate that your strings+satisfy the requirements of the relevant RFCs -- see the note in+"Network.Mail.Assumpta.MonadSmtp#permissiblecharacters"+about "permissible characters". In general, unless you are using+an SMTP extension, your strings must consist of 7-bit clean ASCII.++-}++module Network.Mail.Assumpta.String+ (+ -- * Run Smtp actions+ -- | Run actions in the 'MonadSmtp' monad.+ AT.runSmtp+ , AT.runSecureSmtp+ , AT.runSmtpWithParams+ , AT.runSmtpHandle+ -- * MonadError variants+ -- | Instead of returning an Either, these run in MonadError --+ -- thus the caller can specialize them to a 'Maybe', 'Either',+ -- or some other 'MonadError' instance as desired.+ , AT.runSmtp'+ , AT.runSecureSmtp'+ , AT.runSmtpWithParams'+ , AT.runSmtpHandle'+ -- * SMTP commands+ -- | Functions for sending commands+ -- to an SMTP server. In general, these are wrappers around+ -- 'command' and 'expect' -- they send some command,+ -- and try to parse a response (throwing an 'SmtpError'+ -- on failure).+ -- See the "Network.Mail.Assumpta.ByteString" module for+ -- documentation of each function.+ , helo+ , ehlo+ , mailFrom+ , rcptTo+ , data_+ , AT.noop+ , AT.quit+ , AT.rset+ , AT.startTLS+ , expn+ , vrfy+ , help+ , sendRawMail+ -- * Server responses+ , AT.expect+ , AT.expectGreeting+ -- * Low-level operations and types+ , AT.command+ , AT.Reply+ , AT.ReplyLine(..)+ , AT.ReplyCode+ , AT.SmtpError(..)+ -- * Monad transformer+ -- | A monad transformer, 'SmtpT', which provides the ability to+ -- send SMTP commands and parse replies, together with operations+ -- on the transformer.+ , AT.SmtpT+ , AT.Smtp+ , AT.liftSmtpT+ , AT.mapSmtpT+ -- * Network operations+ , AT.open+ , AT.openTls+ , AT.openParams+ , AT.close+ , AT.withHandle+ , AT.withSecureHandle+ , AT.withHandleParams+ , AT.HostName+ , AT.Port+ , AT.Handle(..)+ , AT.defaultTLSSettings+ , send+ , sendLine+ -- * Utility functions+ , toCrLf+ , AT.rethrow+ , AT.mkParams+ , AT.toIOError+ , AT.crlf+ , escapePeriods+ )+ where++import Data.Foldable (toList)+import Data.Monoid -- needed for early versions of Base+import qualified Data.Text as T++import qualified Network.Mail.Assumpta.ByteString as ABS+import qualified Network.Mail.Assumpta.Text as AT++import Network.Mail.Assumpta.MonadSmtp ( MonadSmtp )+import qualified Network.Mail.Assumpta.MonadSmtp as M++-- | See 'M.helo'+helo :: MonadSmtp m => String -> m () +helo = AT.helo . T.pack++-- | See 'M.ehlo'+ehlo :: MonadSmtp m => String -> m () +ehlo = AT.ehlo . T.pack++-- | See 'M.mailFrom'+mailFrom :: MonadSmtp m => String -> m ()+mailFrom = AT.mailFrom . T.pack++-- | See 'M.rcptTo'+rcptTo :: MonadSmtp m => String -> m ()+rcptTo = AT.rcptTo . T.pack++-- | See 'M.data_'+data_ :: MonadSmtp m => String -> m ()+data_ = AT.data_ . T.pack++-- | See 'M.expn'+expn :: MonadSmtp m => String -> m AT.Reply+expn = AT.expn . T.pack++-- | See 'M.vrfy'+vrfy :: MonadSmtp m => String -> m AT.Reply+vrfy = AT.vrfy . T.pack++help :: MonadSmtp m => Maybe String -> m AT.Reply+help = AT.help . fmap T.pack++-- | See 'M.send'+send :: MonadSmtp m => String -> m ()+send = AT.send . T.pack++-- | See 'M.sendLine'+sendLine :: MonadSmtp m => String -> m ()+sendLine = AT.sendLine . T.pack++-- | replace newlines (@\'\\n'@) with crlf sequence (@\'\\r\\n'@).+toCrLf :: String -> String+toCrLf = T.unpack . AT.toCrLf . T.pack++-- | Where a period ('.') character starts a (crlf-delimited)+-- line, replace it with two periods.+escapePeriods :: String -> String+escapePeriods = T.unpack . AT.escapePeriods . T.pack++-- | see 'ABS.sendRawMail'. +sendRawMail ::+ (MonadSmtp m, Foldable t) => String -> t String -> String -> m ()+sendRawMail sender recipients message =+ let sender' = T.pack sender+ recipients' = map T.pack (toList recipients)+ message' = T.pack message+ in AT.sendRawMail sender' recipients' message'++
+ src/Network/Mail/Assumpta/Text.hs view
@@ -0,0 +1,299 @@+{-# LANGUAGE OverloadedStrings #-}++{- |++'Text' versions of Smtp operations.++This module does not validate that your text+satisfies the requirements of the relevant RFCs -- see the note in+"Network.Mail.Assumpta.MonadSmtp#permissiblecharacters"+about "permissible characters". In general, unless you are using+an SMTP extension, your text must consist of 7-bit clean ASCII.++== Example code++Short example (which will work if you have an SMTP server+running on local port 2025):+(see the+<https://hackage.haskell.org/package/assumpta/src/examples/ examples>+directory for a copy of the code):++> {-# LANGUAGE OverloadedStrings #-}+> +> -- Program that runs a short SMTP session+> -- with a server running on local port 2025.+> +> module Main where+> +> import Data.Monoid+> import qualified Data.Text as T+> import Network.BSD (getHostName) -- requires 'network' package+> import Network.Mail.Assumpta.Text+> +> sampleMesg :: T.Text+> sampleMesg = T.intercalate crlf [+> "Date: Tue, 21 Jan 2020 02:28:37 +0800"+> , "To: neddie.seagoon@gmail.com"+> , "From: jim.moriarty@gmail.com"+> , "Subject: test Tue, 21 Jan 2020 02:28:37 +0800"+> , ""+> , "Sapristi nyuckoes!"+> ]+> +> -- The sender and recipient supplied to the+> -- SMTP server are what is used to route the+> -- message; any 'To:' and 'From:' fields+> -- in the message are completely ignored for this+> -- purpose.+> sender, recipient :: T.Text+> sender = "somesender@mycorp.com"+> recipient = "somerecipient@mozilla.org"+> +> main :: IO ()+> main = do+> let +> port = 2025+> hostname <- getHostName+> print =<< runSmtp "localhost" port (do+> expectGreeting+> ehlo $ T.pack hostname+> -- Properly, we should escape periods at the start of a+> -- line. But we know there aren't any+> sendRawMail sender [recipient] sampleMesg+> quit)++== Alternatives to "connection"++If you want to use other network libraries besides+<https://hackage.haskell.org/package/connection connection>,+it should be pretty straightforward to adapt the code here.++If you want to use stream-like IO, one possibility is+the <https://hackage.haskell.org/package/conduit-connection conduit-connection> package,+which provides Conduit-based sources and sinks on top of "Network.Connection".+++-}++module Network.Mail.Assumpta.Text+ (+ -- * Run Smtp actions+ -- | Run actions in the 'MonadSmtp' monad.+ ABS.runSmtp+ , ABS.runSecureSmtp+ , ABS.runSmtpWithParams+ , ABS.runSmtpHandle+ -- * MonadError variants+ -- | Instead of returning an Either, these run in MonadError --+ -- thus the caller can specialize them to a 'Maybe', 'Either',+ -- or some other 'MonadError' instance as desired.+ , ABS.runSmtp'+ , ABS.runSecureSmtp'+ , ABS.runSmtpWithParams'+ , ABS.runSmtpHandle'+ -- * SMTP commands+ -- | Functions for sending commands+ -- to an SMTP server. In general, these are wrappers around+ -- 'command' and 'expect' -- they send some command,+ -- and try to parse a response (throwing an 'SmtpError'+ -- on failure).+ -- See the "Network.Mail.Assumpta.ByteString" module for+ -- documentation of each function.+ , helo+ , ehlo+ , mailFrom+ , rcptTo+ , data_+ , ABS.noop+ , ABS.quit+ , ABS.rset+ , ABS.startTLS+ , expn+ , vrfy+ , help+ , sendRawMail+ , simpleMail+ -- * Server responses+ , ABS.expect+ , ABS.expectGreeting+ -- * Low-level operations and types+ , ABS.command+ , ABS.Reply+ , ABS.ReplyLine(..)+ , ABS.ReplyCode+ , ABS.SmtpError(..)+ -- * Monad transformer+ -- | A monad transformer, 'SmtpT', which provides the ability to+ -- send SMTP commands and parse replies, together with operations+ -- on the transformer.+ , ABS.SmtpT+ , ABS.Smtp+ , ABS.liftSmtpT+ , ABS.mapSmtpT+ -- * Network operations+ , ABS.open+ , ABS.openTls+ , ABS.openParams+ , ABS.close+ , ABS.withHandle+ , ABS.withSecureHandle+ , ABS.withHandleParams+ , ABS.HostName+ , ABS.Port+ , ABS.Handle(..)+ , ABS.defaultTLSSettings+ , send+ , sendLine+ -- * Utility functions+ , toCrLf+ , ABS.rethrow+ , ABS.mkParams+ , ABS.toIOError+ , ABS.crlf+ , escapePeriods+ )+ where++import Data.ByteString.Lazy (toStrict)+import Data.Foldable (toList)+import Data.Monoid -- needed for early versions of Base+import qualified Data.Text as T+import Data.Text ( Text )+import Data.Text.Encoding ( encodeUtf8, decodeUtf8 )+import Data.Text.Lazy (fromStrict)+import qualified Network.Mail.Mime as MM ( Address(..), simpleMail+ , renderMail' )++import qualified Network.Mail.Assumpta.ByteString as ABS++import Network.Mail.Assumpta.MonadSmtp ( MonadSmtp )+import qualified Network.Mail.Assumpta.MonadSmtp as M++-- | See 'M.helo'+helo :: MonadSmtp m => Text -> m () +helo = M.helo . encodeUtf8++-- | See 'M.ehlo'+ehlo :: MonadSmtp m => Text -> m () +ehlo = M.ehlo . encodeUtf8++-- | See 'M.mailFrom'+mailFrom :: MonadSmtp m => Text -> m ()+mailFrom = M.mailFrom . encodeUtf8++-- | See 'M.rcptTo'+rcptTo :: MonadSmtp m => Text -> m ()+rcptTo = M.rcptTo . encodeUtf8++-- | See 'M.data_'+data_ :: MonadSmtp m => Text -> m ()+data_ = M.data_ . encodeUtf8++-- | See 'M.expn'+expn :: MonadSmtp m => Text -> m M.Reply+expn = M.expn . encodeUtf8++-- | See 'M.vrfy'+vrfy :: MonadSmtp m => Text -> m M.Reply+vrfy = M.vrfy . encodeUtf8++-- | See 'M.help'+help :: MonadSmtp m => Maybe Text -> m M.Reply+help = M.help . fmap encodeUtf8+++-- | See 'M.send'+send :: MonadSmtp m => Text -> m ()+send = M.send . encodeUtf8++-- | See 'M.sendLine'+sendLine :: MonadSmtp m => Text -> m ()+sendLine = M.sendLine . encodeUtf8++-- | replace newlines (@\'\\n'@) with crlf sequence (@\'\\r\\n'@).+toCrLf :: Text -> Text+toCrLf txt = T.intercalate M.crlf $ T.lines txt++-- | Where a period ('.') character starts a (crlf-delimited)+-- line, replace it with two periods.+escapePeriods :: Text -> Text+escapePeriods txt =+ T.concat $ map escapeLine $ T.splitOn M.crlf txt+ where+ escapeLine t =+ if "." `T.isPrefixOf` t+ then "." <> t+ else t++-- | see 'ABS.sendRawMail'. +sendRawMail ::+ (MonadSmtp m, Foldable t) => Text -> t Text -> Text -> m ()+sendRawMail sender recipients message =+ let sender' = encodeUtf8 sender+ recipients' = map encodeUtf8 (toList recipients)+ message' = encodeUtf8 message+ in ABS.sendRawMail sender' recipients' message'+++-- | A simple interface for generating an email with HTML and plain-text+-- alternatives and some file attachments and sending it via an+-- SMTP server.+--+-- Uses lazy IO for reading in the attachment contents. Simple wrapper+-- around 'MM.renderMail''. /Caution/: Not tested, use with care.+-- Likely to change.+--+-- sample use:+--+-- @ +-- > import qualified Network.Mail.Mime as MM+-- > :set -XOverloadedStrings+-- > :{+-- let addr = MM.Address (Just "joe") "joe@nowhere"+-- subj = "a test subject"+-- body = "a test body"+-- > :}+-- > simpleMail addr addr subj body "" [] "myserver.mydomain.com" "mail.yourserver.org" 2025+-- @+{-# WARNING simpleMail "experimental, likely to change" #-}+simpleMail :: MM.Address -- ^ to+ -> MM.Address -- ^ from+ -> T.Text -- ^ subject+ -> T.Text -- ^ plain body+ -> T.Text -- ^ HTML body+ -> [(T.Text, FilePath)] -- ^ content type and path of attachments+ -> String -- ^ qualified name of local host+ -> String -- ^ SMTP server to connect to+ -> Int -- ^ Port of SMTP server+ -> IO (T.Text, Either M.SmtpError ())+simpleMail to from subject plainBody htmlBody attachments localHost server port =+ do+ let plainBody' = fromStrict plainBody+ htmlBody' = fromStrict htmlBody+ mail <- escape . toStrict <$> (MM.renderMail' =<< + MM.simpleMail to from subject plainBody' htmlBody' attachments)+ let mail' = if crlf `T.isSuffixOf` mail+ then mail <> "." <> crlf + else mail <> crlf <> "." <> crlf+ to' = MM.addressEmail to+ from' = MM.addressEmail from+ res <- ABS.runSmtp server port $ do+ ABS.expectGreeting+ ehlo $ T.pack localHost+ sendRawMail from' [to'] mail'+ ABS.quit+ return (mail, res)+ where+ escape = escapePeriods . toCrLf . decodeUtf8 + crlf = ABS.crlf++----testX :: IO (Either SmtpError ())+--testX =+-- let addr = MM.Address (Just "joe") "joe@place"+-- subj = "a test subj"+-- body = "a test body"+-- in simpleMail addr addr subj body "" [] "myserver.lan" "localhost" 2025++++
+ stack-lts-12.26.yaml view
@@ -0,0 +1,7 @@+resolver: lts-12.26++packages:+- .++extra-deps:+- assumpta-core-0.1.0.2@sha256:256ef9a9450c40b0623cfe9f222b32733055a5aee4e4a6658d339aef14f9e3fc
+ test-hspec/Network/Mail/Assumpta/ByteStringSpec.hs view
@@ -0,0 +1,175 @@++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}++module Network.Mail.Assumpta.ByteStringSpec+ (main, spec)+ where++import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.IO (propertyIO)++import Control.Concurrent+import Control.Exception (bracket, evaluate)+import Control.Monad++import qualified Data.ByteString.Char8 as BSC+import Data.ByteString.Char8 ( ByteString )+import Data.Monoid -- for early base++import Network.Socket as N hiding (recv)+import Network.Socket.ByteString as N (sendAll, recv)+++import Network.Mail.Assumpta.ByteString++{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}++-- oops+deriving instance Eq ReplyLine+deriving instance Eq SmtpError++-- .. probably shouldn't churn through *quite* so many sockets+-- so fast - but I haven't run out, so a pool doesn't seem+-- necessary yet.+mkServerSoc :: IO (N.Socket, N.PortNumber)+mkServerSoc = do+ -- wildcard port+ let addr = N.SockAddrInet N.aNY_PORT N.iNADDR_ANY+ soc <- N.socket N.AF_INET N.Stream N.defaultProtocol+ N.bind soc addr+ N.listen soc 5+ -- find out what port we got+ port <- N.socketPort soc+ return (soc,port)++-- | @runSmtpWithServer a replies@+--+-- spin up a TCP server, run 'runSmtp' to send+-- commands to it, return result, plus requests received+-- by server. @replies@ are the canned responses+-- the server is to make.+--+-- The client needs to 'expect' something to start+-- with (i.e. the server goes first), else+-- things are likely to get out of sync.+runSmtpWithServer ::+ Smtp a -> [ByteString] -> IO (Either SmtpError a, ByteString)+runSmtpWithServer commands replies = do+ (soc, port) <- mkServerSoc+ mRequestsRecvd <- newEmptyMVar+ -- server+ void $ forkIO $+ let acquire = fst <$> N.accept soc+ release = N.close+ in bracket acquire release $ \soc' -> do+ requests <- forM replies $ \reply -> do+ N.sendAll soc' $ reply <> crlf+ N.recv soc' 4096+ -- ??? needed??+ requests <- evaluate requests+ --+ putMVar mRequestsRecvd $ mconcat requests+ clientRes <- runSmtp "localhost" (fromIntegral port) commands+ -- return results+ requestsRcvd <- readMVar mRequestsRecvd+ return (clientRes, requestsRcvd)++-- make a server test case out of description, cmds to run, etc+mkSmtpTestCase ::+ (Show b, Eq b) =>+ String+ -> Smtp b+ -> [ByteString]+ -> (Either SmtpError b, ByteString)+ -> SpecWith ()+mkSmtpTestCase testCaseName cmds serverReplies expectedResult =+ context ("when run with test case " <> show testCaseName) $+ it "should send expected bytes & return expected result" $+ runSmtpWithServer cmds serverReplies `shouldReturn` expectedResult++-- | when the client disconnects, the server should get a+-- final empty bytestring+smtpTestCases :: [SpecWith ()]+smtpTestCases = [+ mkSmtpTestCase+ "helo then quit"+ -- cmds+ (do expectGreeting+ helo "myhost"+ quit )+ -- responses+ [ "220 hi there"+ , "250 OK"+ , "221 Bye"+ ]+ -- expect+ (Right (), mconcat ["HELO myhost\r\n"+ ,"QUIT\r\n"])++ , mkSmtpTestCase+ "improper greeting"+ -- cmds+ expectGreeting+ -- responses+ [ "250 OK" -- bad greeting+ ]+ -- expect+ (Left (UnexpectedResponse "220" [ReplyLine 250 "OK"]), "")++ , mkSmtpTestCase+ "using sendRawMail"+ -- cmds+ (do expectGreeting+ sendRawMail "somesender" ["somerecipt"] "some body text"+ quit )+ -- responses+ [ "220 hi there"+ , "250 OK"+ , "250 OK"+ , "354 End data with <CR><LF>.<CR><LF>"+ , "250 OK"+ , "221 Bye"+ ]+ -- expect+ (Right (), BSC.intercalate crlf [+ "MAIL FROM:<somesender>"+ , "RCPT TO:<somerecipt>"+ , "DATA"+ , "some body text"+ , "."+ , "QUIT\r\n"])++ ]++data_Spec :: SpecWith ()+data_Spec = + describe "data_ function" $+ it "should pass on its content unchanged" $ property $+ \(NonEmpty someData) -> ioProperty $ do+ let someData' = BSC.pack someData + cmds = do expectGreeting+ data_ $ someData' <> crlf+ serverReplies = [+ "220 hi there"+ , "354 End data with <CR><LF>.<CR><LF>"+ , "250 OK"+ ]+ res <- runSmtpWithServer cmds serverReplies+ return $ propertyIO $+ res `shouldBe` (Right (), "DATA\r\n" <> someData' <> "\r\n")+++-- `main` is here so that this module can be run from GHCi on its own. It is+-- not needed for automatic spec discovery.+main :: IO ()+main = hspec spec++spec :: Spec+spec = do+ describe "runSmtp" $ + sequence_ smtpTestCases+ data_Spec+
+ test-hspec/hspec-tests.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test-other/compile-examples.hs view
@@ -0,0 +1,80 @@++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExtendedDefaultRules #-}+{-# LANGUAGE LambdaCase #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}++import Control.Monad+import Data.Char as Ch+import Data.Maybe+import Data.Monoid+import Shelly as Sh+import qualified Data.Text as T+import System.IO++-- Requires the test-doctests flag to be enabled,+-- and the STACK_RESOLVER environment variable to+-- be defined (specifying a resolver to use, e.g.+-- "lts-14").+--+default (T.Text)++{-# ANN module ("HLint: ignore Eta reduce" :: String) #-}+{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}++isHaskSource :: Sh.FilePath -> Bool+isHaskSource p = "hs" `hasExt` p++basename :: Sh.FilePath -> Sh.FilePath -> Sh Sh.FilePath+basename dir file =+ relativeTo dir file++baseNameSatisfies ::+ (T.Text -> b) -> Sh.FilePath -> Sh.FilePath -> Sh b+baseNameSatisfies pred basedir file = pred . toTextIgnore <$> basename basedir file++-- is stack on the path+we_must_have_stack :: Sh ()+we_must_have_stack =+ unlessM (isJust <$> which "stack") $ do+ echo "'stack' binary not found on path, exiting"+ terror "abort! abort!" ++-- are we running as part of 'stack test'?+-- If so, we can assume the library has been+-- built and is available to us.+we_must_be_run_by_stack :: Sh ()+we_must_be_run_by_stack =+ unlessM (isJust <$> get_env "STACK_EXE") $+ terror "STACK_EXE env var not set"++get_stack_resolver :: Sh T.Text+get_stack_resolver =+ get_env "STACK_RESOLVER" >>= \case+ Nothing -> terror "STACK_RESOLVER env var not set, exiting"+ Just res -> return res++main :: IO ()+main = + do+ let examplesDir = "examples"+ hSetBuffering stdout LineBuffering+ shelly $ verbosely $ do+ echo "\n\n=================="+ echo "Compiling Examples"+ echo "==================\n"+ stack_resolver <- get_stack_resolver+ we_must_have_stack+ we_must_be_run_by_stack+ hsFiles <- (filter isHaskSource <$> ls examplesDir) >>=+ filterM (baseNameSatisfies startsWithLetter examplesDir)++ forM_ hsFiles $ \hsFile -> do+ let hsFile' = toTextIgnore hsFile + echo $ "Compiling " <> hsFile' <> "\n"+ run "stack" ["ghc", "--resolver=" <> stack_resolver+ , "--", "--make", toTextIgnore hsFile]++ where+ startsWithLetter = Ch.isLetter . T.head+