packages feed

purebred-email 0.4.1 → 0.4.2

raw patch · 4 files changed

+75/−66 lines, 4 filesdep ~semigroupsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: semigroups

API changes (from Hackage documentation)

+ Data.RFC5322: instance GHC.Classes.Eq Data.RFC5322.PhraseEscapeRequirement
+ Data.RFC5322: instance GHC.Classes.Ord Data.RFC5322.PhraseEscapeRequirement

Files

purebred-email.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/  name:                purebred-email-version:             0.4.1+version:             0.4.2 synopsis:            types and parser for email messages (including MIME) description:   The purebred email library.  RFC 5322, MIME, etc.@@ -74,7 +74,7 @@     , deepseq >= 1.4.2     , lens >= 4 && < 5     , semigroupoids >= 5 && < 6-    , semigroups >= 0.16 && < 0.19+    , semigroups >= 0.16     , stringsearch >= 0.3     , text >= 1.2     , time
src/Data/RFC5322.hs view
@@ -270,42 +270,40 @@ buildPhrase s =   case enc s of     PhraseAtom -> T.encodeUtf8Builder s-    PhraseQuotedString _ -> "\"" <> T.encodeUtf8BuilderEscaped escPrim s <> "\""+    PhraseQuotedString -> qsBuilder False+    PhraseQuotedStringEscapeSpace -> qsBuilder True     PhraseEncodedWord -> buildEncodedWord . transferEncode . charsetEncode $ s   where-    enc = T.foldr (\c z -> encChar c <> z) mempty-    encChar c+    enc = snd . T.foldr (\c (prev, req) -> (c, encChar prev c <> req)) ('\0', mempty)+    encChar prev c       | isAtext c = PhraseAtom-      | isQtext c = PhraseQuotedString 0-      | isVchar c || c == ' ' = PhraseQuotedString 1+      | isQtext c = PhraseQuotedString+      | isVchar c = PhraseQuotedString+      | c == ' ' =+          if prev == ' '  -- two spaces in a row; need to avoid FWS+          then PhraseQuotedStringEscapeSpace+          else PhraseQuotedString       | otherwise = PhraseEncodedWord -    -- FIXME: this probably doesn't handle consecutive SPACE properly-    -- due to FWS:-    ---    --    quoted-string   =   [CFWS]-    --                        DQUOTE *([FWS] qcontent) [FWS] DQUOTE-    --                        [CFWS]-    ---    -- Do not be surprised if the roundtrip property fails-    ---    escPrim = Prim.condB (\c -> isQtext c || c == 32)+    qsBuilder escSpace = "\"" <> T.encodeUtf8BuilderEscaped (escPrim escSpace) s <> "\""+    escPrim escSpace = Prim.condB (\c -> isQtext c || not escSpace && c == 32)       (Prim.liftFixedToBounded Prim.word8)       (Prim.liftFixedToBounded $ (fromChar '\\',) Prim.>$< Prim.word8 Prim.>*< Prim.word8)  -- | Data type used to compute escaping requirement of a Text 'phrase'--- 'PhraseQuotedString' records the number of additional characters--- needed for escapes (backslash).  It does not include the surrounding--- DQUOTE characters. ---data PhraseEscapeRequirement = PhraseAtom | PhraseQuotedString Int | PhraseEncodedWord+data PhraseEscapeRequirement+  = PhraseAtom+  | PhraseQuotedString+  | PhraseQuotedStringEscapeSpace+  | PhraseEncodedWord+  deriving (Eq, Ord)  instance Semigroup PhraseEscapeRequirement where-  PhraseEncodedWord <> _ = PhraseEncodedWord-  PhraseAtom <> a = a-  PhraseQuotedString n <> PhraseQuotedString m = PhraseQuotedString (n + m)-  a <> PhraseAtom = a-  _ <> PhraseEncodedWord = PhraseEncodedWord+  PhraseEncodedWord <> _ =+    -- allows early termination of folds+    PhraseEncodedWord+  l <> r = max l r  instance Monoid PhraseEscapeRequirement where   mempty = PhraseAtom@@ -446,35 +444,36 @@   let key = original k   in     Builder.byteString key-    <> ": "-    <> Builder.byteString (foldUnstructured (B.length key) v)+    <> ":"+    <> foldUnstructured v (B.length key + 1)     <> "\r\n" - -- | Render a field body with proper folding ----- Algorithm:--- * Break the string on white space--- * Use a counter which indicates a new folding line if it exceeds 77 characters--- * Whenever we create a new line, concatenate all words back with white space and push it into the result--- * The result is a list of byte strings, which is concatenated with \r\n\s------ Notes:---  * First take at this, so possibly very inefficient---  * No other delimiters (e.g. commas, full stops, etc) are considered for---    folding other than whitespace---  * Attaches an additional whitespace when joining+-- Folds on whitespace (and only whitespace).  Sequential whitespace+-- chars are folded.  That's OK because the grammar says it is+-- folding whitespace. ---foldUnstructured :: Int -> B.ByteString -> B.ByteString-foldUnstructured i b =-    let xs = chunk (i + 2) (Char8.words b) [] []-    in B.intercalate "\r\n " (filter (not . B.null) xs)--chunk :: Int -> [B.ByteString] -> [B.ByteString] -> [B.ByteString] -> [B.ByteString]-chunk _ [] xs result = result <> [Char8.unwords xs]-chunk max' (x:rest) xs result = if (max' + B.length x + 1) >= 77-                                then result <> [Char8.unwords xs] <> chunk (B.length x + 1) rest [x] []-                                else result <> chunk (max' + B.length x + 1) rest (xs <> [x]) result+foldUnstructured :: B.ByteString -> Int -> Builder.Builder+foldUnstructured s i = case Char8.words s of+  [] -> mempty+  (h:t) ->+    -- Special case to prevent wrapping of first word;+    -- see 6dbc04fb1863e845699b1cef50f4edaf1326bdae for info.+    " " <> Builder.byteString h <> go t (i + 1 + B.length h)+  where+  limit = 76  -- could be 78, but this preserves old behaviour+  go [] _ = mempty+  go (chunk:chunks) col+    | col + 1 + B.length chunk < limit =+        -- there is room for the chunk+        " " <> Builder.byteString chunk <> go chunks (col + 1 + B.length chunk)+    | col <= 1 =+        -- there isn't room for the chunk, but we are at the+        -- beginning of the line so add it here anyway (otherwise+        -- we will add "\r\n" and recurse forever+        " " <> Builder.byteString chunk <> "\r\n" <> go chunks 0+    | otherwise = "\r\n" <> go (chunk:chunks) 0  -- fold  -- | Printable ASCII excl. ':' isFtext :: Word8 -> Bool
tests/Headers.hs view
@@ -8,16 +8,16 @@ import Data.Semigroup ((<>)) import Data.Word (Word8) -import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString as B-import Data.ByteString.Lazy (toStrict)+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as L8 import Data.Attoparsec.ByteString.Char8 (parseOnly) import qualified Data.Attoparsec.Text as AText (parseOnly) import qualified Data.ByteString.Builder as Builder import qualified Data.CaseInsensitive as CI import Data.Either (isLeft) -import Test.Tasty (TestTree, testGroup)+import Test.Tasty import Test.Tasty.HUnit (assertBool, (@=?), (@?=), testCase, Assertion) import Test.Tasty.QuickCheck import Test.QuickCheck.Instances ()@@ -26,8 +26,8 @@ import qualified Data.RFC5322.Address.Text as AddressText   (mailbox, address, renderAddress) -renderField :: (CI.CI B.ByteString, B.ByteString) -> B.ByteString-renderField = toStrict . Builder.toLazyByteString . buildField+renderField :: (CI.CI B.ByteString, B.ByteString) -> L.ByteString+renderField = Builder.toLazyByteString . buildField  unittests :: TestTree unittests = testGroup "Headers"@@ -345,15 +345,21 @@ genField :: Gen (CI.CI B.ByteString, B.ByteString) genField = (,) <$> (CI.mk <$> genFieldItem) <*> genFieldBody -newtype MailHeaders = MailHeaders { unHeader :: (CI.CI B.ByteString, B.ByteString)}-  deriving (Show)--instance Arbitrary MailHeaders where-    arbitrary = MailHeaders <$> genField+prop_renderHeadersRoundtrip :: Property+prop_renderHeadersRoundtrip = forAll genField $ \kv ->+  parse field (renderField kv) == Right kv -prop_renderHeadersRoundtrip :: MailHeaders -> Bool-prop_renderHeadersRoundtrip h = parse field (renderField (unHeader h)) == Right (unHeader h)+prop_foldedUnstructuredLimited :: Property+prop_foldedUnstructuredLimited = forAll genField $ \kv ->+  all ((<= 78) . L.length) (crlfLines $ renderField kv) -prop_foldedUnstructuredLimited :: MailHeaders -> Bool-prop_foldedUnstructuredLimited h = let xs = BC.lines $ renderField (unHeader h)-                                   in all (== True) ((\x -> B.length x <= 78) <$> xs)+crlfLines :: L.ByteString -> [L.ByteString]+crlfLines = go ""+  where+  go acc s =+    let (h,t) = L8.span (/= '\r') s+    in+      case L.take 2 t of+        ""      -> [acc <> h]+        "\r\n"  -> acc <> h : go "" (L.drop 2 t)+        _       -> go (acc <> h <> L.take 1 t) (L.drop 1 t)
tests/Message.hs view
@@ -27,7 +27,7 @@  import Data.Char (isPrint) import Data.Foldable (fold)-import Data.List.NonEmpty (NonEmpty, intersperse)+import Data.List.NonEmpty (NonEmpty(..), intersperse)  import Control.Lens (set, view) import qualified Data.ByteString as B@@ -47,6 +47,10 @@   [ testProperty "message round trip" prop_messageRoundTrip   , localOption (HedgehogTestLimit (Just 10000)) $       testProperty "message round trip with From header" prop_messageFromRoundTrip+  , testProperty "mailbox with consecutive spaces" $+      -- https://github.com/purebred-mua/purebred-email/issues/56+      let m = Mailbox (Just "  ") (AddrSpec "!" (DomainDotAtom ("!" :| [])))+      in withTests 1 . property . assert $ renderMailbox m == "\"\\ \\ \" <!@!>"   ]  printableAsciiChar, printableUnicodeChar, unicodeCharAsciiBias :: Gen Char