diff --git a/purebred-email.cabal b/purebred-email.cabal
--- a/purebred-email.cabal
+++ b/purebred-email.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                purebred-email
-version:             0.5
+version:             0.5.1
 synopsis:            types and parser for email messages (including MIME)
 description:
   The purebred email library.  RFC 5322, MIME, etc.
@@ -36,7 +36,7 @@
   test-vectors/*.eml
   tests/golden/*.golden
 tested-with:
-  GHC ==8.8.4 || ==8.10.4 || ==9.0.1
+  GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.3 || ==9.4.1
 
 homepage:            https://github.com/purebred-mua/purebred-email
 bug-reports:         https://github.com/purebred-mua/purebred-email/issues
@@ -54,11 +54,10 @@
 
   build-depends:
     base >= 4.11 && < 5
-    , attoparsec >= 0.13 && < 0.14
-    , bytestring >= 0.10 && < 0.11
+    , attoparsec >= 0.13 && < 0.15
+    , bytestring >= 0.10 && < 0.12
     , case-insensitive >= 1.2 && < 1.3
     , lens >= 4 && < 6
-    , semigroups >= 0.16
     , text >= 1.2
     , time >= 1.9
 
diff --git a/src/Data/IMF.hs b/src/Data/IMF.hs
--- a/src/Data/IMF.hs
+++ b/src/Data/IMF.hs
@@ -387,7 +387,36 @@
 
 phrase :: CharsetLookup -> Parser T.Text
 phrase charsets = foldMany1Sep " " $
-  fmap (decodeEncodedWord charsets) ("=?" *> encodedWord)
+  -- RFC 2047 §2: if it is desirable to encode more text than will
+  -- fit in an 'encoded-word' of 75 characters, multiple
+  -- 'encoded-word's (separated by CRLF SPACE) may be used.
+  --
+  -- The initial header parsing unfolds the header, so such
+  -- "continuation" encoded-words are now separated by SPACE.  The
+  -- CRLFs have been erased.  Naïvely, this seems to make this case
+  -- indistinguishable from "consecutive" encoded-words that were
+  -- actually separated by SPACE.  However, a careful examination of
+  -- the grammar shows that encoded-words in a 'phrase' cannot be
+  -- separated by whitespace:
+  --
+  -- @
+  -- phrase         = 1*( encoded-word / word )
+  -- encoded-word   = "=?" charset "?" encoding "?" encoded-text "?="
+  -- word           = atom / quoted-string
+  -- atom           = [CFWS] 1*atext [CFWS]
+  -- quoted-string  = [CFWS]
+  --                  DQUOTE *([FWS] qcontent) [FWS] DQUOTE
+  --                  [CFWS]
+  -- @
+  --
+  -- The only place whitespace is allowed is within 'atom' and
+  -- 'quoted-string'.  Therefore two encoded-words separated by
+  -- SPACE must be the result of folding a long encoded-word.  So
+  -- consume as many SPACE separated encoded-words as possible,
+  -- decode them, and concatenate the result.
+  fmap
+    ( foldMap (decodeEncodedWord charsets) )
+    ( ("=?" *> encodedWord) `sepBy1` char8 ' ' )
   <|> fmap decodeLenient word
 
 displayName :: CharsetLookup -> Parser T.Text
diff --git a/src/Data/IMF/Syntax.hs b/src/Data/IMF/Syntax.hs
--- a/src/Data/IMF/Syntax.hs
+++ b/src/Data/IMF/Syntax.hs
@@ -14,7 +14,6 @@
 -- You should have received a copy of the GNU Affero General Public License
 -- along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE FunctionalDependencies #-}
@@ -77,7 +76,7 @@
   ) where
 
 import Prelude hiding (takeWhile)
-import Control.Applicative ((<|>), Alternative, liftA2, many, optional)
+import Control.Applicative ((<|>), Alternative, liftA2, many, optional, some)
 import Control.Monad (void)
 import qualified Data.Attoparsec.ByteString as A
 import qualified Data.Attoparsec.Internal as A
@@ -98,11 +97,7 @@
 
 -- | Constraint synonym to handle the Semigroup Monoid Proposal
 -- transition gracefully.
-#if MIN_VERSION_base(4,11,0)
 type SM a = Monoid a
-#else
-type SM a = (Semigroup a, Monoid a)
-#endif
 
 class IsChar a where
   toChar :: a -> Char
@@ -196,8 +191,24 @@
 -- | Folding white space (FWS).  A run of one or more whitespace
 -- characters.  Returns a single SPACE character.
 fws :: (Alternative (f s), CharParsing f s a) => (f s) s
-fws = ( optional (takeWhile isWsp *> crlf) *> takeWhile1 isWsp )
-      $> singleton ' '
+fws =
+  -- obs-FWS is more permissive, so must come first.  This means
+  -- that the second branch is unused, but keep it anyway for
+  -- completeness.
+  obsFWS
+  <|>
+  --    FWS             =   ([*WSP CRLF] 1*WSP) /  obs-FWS
+  optional (takeWhile isWsp *> crlf) *> takeWhile1 isWsp $> singleton ' '
+
+-- | Obsolete Folding White Space:
+-- https://www.rfc-editor.org/errata/eid1908
+--
+-- @
+-- obs-FWS         =   1*([CRLF] WSP)
+-- @
+--
+obsFWS :: (Alternative (f s), CharParsing f s a) => (f s) s
+obsFWS = some (optional crlf *> wsp) $> singleton ' '
 
 -- | FWS collapsed to a single SPACE character, or empty string
 --
diff --git a/src/Data/MIME.hs b/src/Data/MIME.hs
--- a/src/Data/MIME.hs
+++ b/src/Data/MIME.hs
@@ -486,7 +486,7 @@
     (\(Message h' b') -> Message h' (Part b')) <$> f (Message h b)
   Encapsulated msg -> Message h . Encapsulated <$> entities f msg
   Multipart sub b bs ->
-    Message h . Multipart sub b <$> sequenceA (entities f <$> bs)
+    Message h . Multipart sub b <$> traverse (entities f) bs
   FailedParse _ _ -> pure (Message h a)
 
 -- | Leaf entities with @Content-Disposition: attachment@
diff --git a/src/Data/MIME/EncodedWord.hs b/src/Data/MIME/EncodedWord.hs
--- a/src/Data/MIME/EncodedWord.hs
+++ b/src/Data/MIME/EncodedWord.hs
@@ -56,6 +56,8 @@
 import Data.MIME.QuotedPrintable
 import Data.IMF.Syntax (ci, takeTillString)
 
+{-# ANN module ("HLint: ignore Eta reduce" :: String) #-}
+
 data EncodedWord = EncodedWord
   { _encodedWordCharset :: CI.CI B.ByteString
   , _encodedWordLanguage :: Maybe (CI.CI B.ByteString)
diff --git a/tests/Parser.hs b/tests/Parser.hs
--- a/tests/Parser.hs
+++ b/tests/Parser.hs
@@ -20,7 +20,8 @@
 
 import Data.Either (isLeft)
 
-import Data.Attoparsec.ByteString.Lazy as AL
+import Data.Attoparsec.ByteString.Lazy as AL hiding (parseOnly)
+import Data.Attoparsec.ByteString (parseOnly)
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Char8 as Char8
 import qualified Data.ByteString.Lazy as L
