diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2016, Michal Kawalec
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Michal Kawalec nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+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
+OWNER 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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/csrc/codec.c b/csrc/codec.c
new file mode 100644
--- /dev/null
+++ b/csrc/codec.c
@@ -0,0 +1,162 @@
+// Copyright: (c) Magnus Therning, 2012
+// License: BSD3, found in the LICENSE file
+
+#include <assert.h>
+#include <string.h>
+
+#include "codec.h"
+
+static uint8_t const qp_decmap[] = {
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+};
+
+int qp_dec_c(uint8_t const *src, size_t srclen,
+    uint8_t *dst, size_t *dstlen,
+    uint8_t const **rem, size_t *remlen)
+{
+    assert(src || srclen == 0);
+    assert(dst);
+    assert(dstlen);
+    assert(rem);
+    assert(remlen);
+
+    size_t od = *dstlen, i;
+    int res = 0;
+
+    for(i = 0, *dstlen = 0; i < srclen && *dstlen < od; i++, (*dstlen)++) {
+        if((9 == src[i]) || (10 == src[i]) || (13 == src[i]) ||
+            (32 <= src[i] && src[i] <= 60) ||
+            (62 <= src[i] && src[i] <= 126)) {
+            dst[*dstlen] = src[i];
+        } else if('=' == src[i]) {
+            if(i + 2 >= srclen) { res = 0; goto exit; }
+
+            // We want to append nothing if we see '=\r\n'
+            if (src[i + 1] == '\r' && src[i + 2] == '\n') {
+              (*dstlen)--;
+            } else {
+              uint8_t o0 = qp_decmap[src[i + 1]], o1 = qp_decmap[src[i + 2]];
+              if((o0 | o1) & 0xf0) { res = 1; break; }
+              dst[*dstlen] = o0 << 4 | o1;
+            }
+
+            i += 2;
+        } else { res = 1; goto exit; }
+    }
+
+exit:
+    *rem = src + i;
+    *remlen = srclen -i;
+
+    return(res);
+}
+
+// decode map, 0x80 = not allowed, 0x40 = end char
+static uint8_t const b64_decmap[] = {
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x3e, 0x80, 0x80, 0x80, 0x3f,
+    0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x80, 0x80, 0x80, 0x40, 0x80, 0x80,
+    0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+    0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
+    0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+    0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 };
+
+
+int b64_dec_part_c(uint8_t const *src, size_t srclen,
+    uint8_t *dst, size_t *dstlen,
+    uint8_t const **rem, size_t *remlen)
+{
+    assert(src || 0 == srclen);
+    assert(dst);
+    assert(dstlen);
+    assert(rem);
+    assert(remlen);
+
+    size_t od = *dstlen, i;
+    int res = 0;
+
+    for(i = 0, *dstlen = 0; i + 4 <= srclen && *dstlen + 3 <= od; i += 4, *dstlen += 3) {
+        uint8_t o0, o1, o2, o3;
+
+        if (src[i] == '\r' && src[i+1] == '\n') {
+          i -= 2;
+          *dstlen -= 3;
+          continue;
+        }
+
+        o0 = b64_decmap[src[i]];
+        o1 = b64_decmap[src[i+1]];
+        o2 = b64_decmap[src[i+2]];
+        o3 = b64_decmap[src[i+3]];
+        if(!(0xc0 & (o0 | o1 | o2 | o3))) { // no illegal chars, and no '='
+            *dst++ = (o0 << 2) | (o1 >> 4);
+            *dst++ = (o1 << 4) | (o2 >> 2);
+            *dst++ = (o2 << 6) | o3;
+        } else if((!(0xc0 & (o0 | o1)) && (0x40 & o2 & o3)) // two legal chars, two '='
+            || (!(0xc0 & (o0 | o1 | o2)) && (0x40 & o3))) { // three legal chars, one '='
+            res = 0;
+            break;
+        } else {
+            res = 1;
+            break;
+        }
+    }
+
+    *rem = src + i;
+    *remlen = srclen - i;
+    return(res);
+}
+
+int b64_dec_final_c(uint8_t const *src, size_t srclen,
+    uint8_t *dst, size_t *dstlen)
+{
+    assert(src || 0 == srclen);
+    assert(dst);
+    assert(dstlen);
+
+    uint8_t o0, o1, o2, o3;
+
+    if(0 == srclen || (src[0] == '\r' && src[1] == '\n')) {
+        *dstlen = 0;
+        return(0);
+    }
+    o0 = b64_decmap[src[0]];
+    o1 = b64_decmap[src[1]];
+    o2 = b64_decmap[src[2]];
+    o3 = b64_decmap[src[3]];
+    if(!(0xc0 & (o0 | o1)) && (0x40 & o2 & o3)) { // two legal chars, two '='
+        *dst++ = (o0 << 2) | (o1 >> 4);
+        *dstlen = 1;
+    } else if(!(0xc0 & (o0 | o1 | o2)) && (0x40 & o3)) { // three legal chars, one '='
+            *dst++ = (o0 << 2) | (o1 >> 4);
+            *dst++ = (o1 << 4) | (o2 >> 2);
+            *dstlen = 2;
+    } else
+        return(1);
+
+    return(0);
+}
diff --git a/emailparse.cabal b/emailparse.cabal
new file mode 100644
--- /dev/null
+++ b/emailparse.cabal
@@ -0,0 +1,95 @@
+-- Initial emailparse.cabal generated by cabal init.  For further
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                emailparse
+version:             0.2.0.6
+synopsis:            An email parser that will parse everything
+-- description:
+license:             BSD3
+license-file:        LICENSE
+author:              Michal Kawalec
+maintainer:          michal@monad.cat
+-- copyright:
+category:            Network
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Network.Mail.Parse
+                       Network.Mail.Parse.Types
+                       Network.Mail.Parse.Utils
+                       Network.Mail.Parse.Decoders.BodyDecoder
+                       Network.Mail.Parse.Decoders.FormatDecoders
+                       Network.Mail.Parse.Parsers.Header
+                       Network.Mail.Parse.Parsers.HeaderFields
+                       Network.Mail.Parse.Parsers.Message
+                       Network.Mail.Parse.Parsers.Multipart
+                       Network.Mail.Parse.Parsers.Utils
+  -- other-modules:
+  -- other-extensions:
+  build-depends:       base >=4.8 && <4.9,
+                       bytestring == 0.10.*,
+                       attoparsec == 0.13.*,
+                       word8 == 0.1.*,
+                       text == 1.2.*,
+                       mime == 0.4.*,
+                       MissingH == 1.3.*,
+                       either == 4.4.*,
+                       text-icu == 0.7.*,
+                       time == 1.5.*,
+                       strptime == 1.0.*,
+                       either-unwrap == 1.1.*
+  hs-source-dirs:      src
+  c-sources:           csrc/codec.c
+  include-dirs:        csrc
+  cc-options:          -fPIC -Wall -Wextra -O2
+  default-language:    Haskell2010
+  ghc-options:         -Wall -fno-warn-unused-do-bind
+  default-extensions:  OverloadedStrings,
+                       GeneralizedNewtypeDeriving,
+                       BangPatterns,
+                       ScopedTypeVariables,
+                       TypeSynonymInstances,
+                       DeriveFunctor,
+                       DeriveGeneric,
+                       FlexibleInstances,
+                       TemplateHaskell,
+                       LambdaCase
+
+Test-Suite emailparse-test
+  type: exitcode-stdio-1.0
+  main-is: main.hs
+  hs-source-dirs: test, src
+  build-depends:       base >=4.8 && <4.9,
+                       bytestring == 0.10.*,
+                       attoparsec == 0.13.*,
+                       word8 == 0.1.*,
+                       text == 1.2.*,
+                       mime == 0.4.*,
+                       MissingH == 1.3.*,
+                       either == 4.4.*,
+                       text-icu == 0.7.*,
+                       time == 1.5.*,
+                       strptime == 1.0.*,
+                       either-unwrap == 1.1.*,
+
+                       tasty == 0.11.*,
+                       HUnit == 1.3.*,
+                       QuickCheck == 2.8.*,
+                       tasty-hunit == 0.9.*,
+                       tasty-quickcheck == 0.8.*,
+                       mtl == 2.2.*
+  c-sources:           csrc/codec.c
+  include-dirs:        csrc
+  cc-options:          -fPIC -Wall -Wextra -O2
+  default-language:    Haskell2010
+  default-extensions:  OverloadedStrings,
+                       GeneralizedNewtypeDeriving,
+                       BangPatterns,
+                       ScopedTypeVariables,
+                       TypeSynonymInstances,
+                       DeriveFunctor,
+                       FlexibleInstances,
+                       TemplateHaskell,
+                       LambdaCase
diff --git a/src/Network/Mail/Parse.hs b/src/Network/Mail/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse.hs
@@ -0,0 +1,15 @@
+module Network.Mail.Parse (parseMessage) where
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Parsers.Message (messageParser)
+
+import qualified Data.ByteString.Char8 as BSC
+import Data.Attoparsec.ByteString
+import Control.Monad (join)
+import qualified Data.Text as T
+import Data.Either.Combinators (mapLeft)
+
+-- |Parses a single message of any mimetype
+parseMessage :: BSC.ByteString -> Either ErrorMessage EmailMessage
+parseMessage message =
+  join . mapLeft T.pack $ parseOnly (messageParser Nothing Nothing) message
diff --git a/src/Network/Mail/Parse/Decoders/BodyDecoder.hs b/src/Network/Mail/Parse/Decoders/BodyDecoder.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Decoders/BodyDecoder.hs
@@ -0,0 +1,66 @@
+module Network.Mail.Parse.Decoders.BodyDecoder where
+
+import qualified Data.ByteString.Char8 as BSC
+
+import Codec.MIME.Parse (parseMIMEType)
+import Codec.MIME.Type
+
+import Data.Either.Combinators (mapLeft, fromRight')
+import Data.Either.Utils (maybeToEither)
+import Data.Either (isRight, rights)
+
+import Data.List (find)
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding (decodeUtf8)
+import qualified Data.Text.ICU.Convert as ICU
+import System.IO.Unsafe (unsafePerformIO)
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Decoders.FormatDecoders (qpDec, decodeB64)
+import Network.Mail.Parse.Utils (findHeader)
+
+
+-- |Remove transfer encoding from a string of bytes
+transferDecode :: BSC.ByteString -> Text -> Either (BSC.ByteString, BSC.ByteString) BSC.ByteString
+transferDecode body encoding = case T.toLower encoding of
+  "quoted-printable" -> qpDec body
+  "q" -> qpDec body
+  "base64" -> decodeB64 body
+  "b" -> decodeB64 body
+  _ -> Right body
+
+-- |Transform a string of bytes with a given encoding
+-- into a UTF-8 string of bytes
+encodingToUtf :: BSC.ByteString -> Text -> Text
+encodingToUtf body encoding = case T.toLower encoding of
+  "utf-8" -> decodeUtf8 body
+  _ -> ICU.toUnicode converter body
+    where converter = unsafePerformIO $ ICU.open (T.unpack encoding) (Just True)
+
+-- |Reverse content transfer encoding applied to the body.
+decodeBody :: [Header] -> BSC.ByteString -> BSC.ByteString
+decodeBody headers body =
+  if isRight decodedBody
+    then fromRight' decodedBody
+    else body
+  where decodedBody = findHeader "Content-Transfer-Encoding" headers >>=
+          return . headerContents >>=
+          \h -> mapLeft (const "Decoding error") (transferDecode body h)
+
+-- |Given a set of headers it tries to figure out
+-- the transfer encoding and charset and normalizes
+-- the contents into an UTF-8 encoded Text.
+--
+-- It will recover from errors, wherever possible
+decodeTextBody :: [Header] -> BSC.ByteString -> Text
+decodeTextBody headers body =
+  if isRight charset
+    then encodingToUtf decodedBody $ fromRight' charset
+    else decodeUtf8 decodedBody
+  where decodedBody = decodeBody headers body
+        charset = findHeader "Content-Type" headers >>=
+          \h -> maybeToEither "" (parseMIMEType $ headerContents h) >>=
+          \m -> maybeToEither "" $ find (\x -> paramName x == "charset") (mimeParams m) >>=
+          return . paramValue
diff --git a/src/Network/Mail/Parse/Decoders/FormatDecoders.hs b/src/Network/Mail/Parse/Decoders/FormatDecoders.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Decoders/FormatDecoders.hs
@@ -0,0 +1,91 @@
+module Network.Mail.Parse.Decoders.FormatDecoders (qpDec, decodeB64) where
+
+-- Copyright: (c) Magnus Therning, 2012
+-- License: BSD3, found in the LICENSE file
+-- Copied from the Sandi library
+
+import Foreign
+import Foreign.C.Types
+import System.IO.Unsafe as U
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Unsafe as BSU
+import Data.ByteString.Unsafe
+
+foreign import ccall "static qp.h qp_dec_c"
+    c_qp_dec :: Ptr Word8 -> CSize -> Ptr Word8 -> Ptr CSize -> Ptr (Ptr Word8) -> Ptr CSize -> IO CInt
+
+foreign import ccall "static b64.h b64_dec_part_c"
+    c_b64_dec_part :: Ptr Word8 -> CSize -> Ptr Word8 -> Ptr CSize -> Ptr (Ptr Word8) -> Ptr CSize -> IO CInt
+
+foreign import ccall "static b64.h b64_dec_final_c"
+    c_b64_dec_final :: Ptr Word8 -> CSize -> Ptr Word8 -> Ptr CSize -> IO CInt
+
+castEnum :: (Enum a, Enum b) => a -> b
+castEnum = toEnum . fromEnum
+
+qpDec' :: BS.ByteString -> Either (BS.ByteString, BS.ByteString) (BS.ByteString, BS.ByteString)
+qpDec' bs = U.unsafePerformIO $ BSU.unsafeUseAsCStringLen bs $ \ (inBuf, inLen) -> do
+    outBuf <- mallocBytes inLen
+    alloca $ \ pOutLen ->
+        alloca $ \ pRemBuf ->
+            alloca $ \ pRemLen -> do
+                poke pOutLen (castEnum inLen)
+                r <- c_qp_dec (castPtr inBuf) (castEnum inLen) outBuf pOutLen pRemBuf pRemLen
+                outLen <- peek pOutLen
+                newOutBuf <- reallocBytes outBuf (castEnum outLen)
+                remBuf <- peek pRemBuf
+                remLen <- peek pRemLen
+                remBs <- BS.packCStringLen (castPtr remBuf, castEnum remLen)
+                outBs <- BSU.unsafePackCStringFinalizer newOutBuf (castEnum outLen) (free newOutBuf)
+                if r == 0
+                    then return $ Right (outBs, remBs)
+                    else return $ Left (outBs, remBs)
+
+qpDec :: BS.ByteString -> Either (BS.ByteString, BS.ByteString) BS.ByteString
+qpDec bs = case qpDec' bs of
+    Right a@(d, r) -> if BS.null r
+            then Right d
+            else Left a
+    Left a -> Left a
+
+b64DecodePart :: BS.ByteString -> Either (BS.ByteString, BS.ByteString) (BS.ByteString, BS.ByteString)
+b64DecodePart bs = U.unsafePerformIO $ unsafeUseAsCStringLen bs $ \ (inBuf, inLen) -> do
+    let maxOutLen = inLen `div` 4 * 3
+    outBuf <- mallocBytes maxOutLen
+    alloca $ \ pOutLen ->
+        alloca $ \ pRemBuf ->
+            alloca $ \ pRemLen -> do
+                poke pOutLen (castEnum maxOutLen)
+                r <- c_b64_dec_part (castPtr inBuf) (castEnum inLen) outBuf pOutLen pRemBuf pRemLen
+                outLen <- peek pOutLen
+                newOutBuf <- reallocBytes outBuf (castEnum outLen)
+                remBuf <- peek pRemBuf
+                remLen <- peek pRemLen
+                remBs <- BS.packCStringLen (castPtr remBuf, castEnum remLen)
+                outBs <- unsafePackCStringFinalizer newOutBuf (castEnum outLen) (free newOutBuf)
+                if r == 0
+                    then return $ Right (outBs, remBs)
+                    else return $ Left (outBs, remBs)
+
+b64DecodeFinal :: BS.ByteString -> Maybe BS.ByteString
+b64DecodeFinal bs = U.unsafePerformIO $ unsafeUseAsCStringLen bs $ \ (inBuf, inLen) -> do
+    outBuf <- mallocBytes 3
+    alloca $ \ pOutLen -> do
+        r <- c_b64_dec_final (castPtr inBuf) (castEnum inLen) outBuf pOutLen
+        if r == 0
+            then do
+                outLen <- peek pOutLen
+                newOutBuf <- reallocBytes outBuf (castEnum outLen)
+                outBs <- unsafePackCStringFinalizer newOutBuf (castEnum outLen) (free newOutBuf)
+                return $ Just outBs
+            else free outBuf >> return Nothing
+
+decodeB64 :: BS.ByteString -> Either (BS.ByteString, BS.ByteString) BS.ByteString
+decodeB64 bs = either
+    Left
+    (\ (first, rest) ->
+        maybe
+            (Left (first, rest))
+            (\ fin -> Right (first `BS.append` fin))
+            (b64DecodeFinal rest))
+    (b64DecodePart bs)
diff --git a/src/Network/Mail/Parse/Parsers/Header.hs b/src/Network/Mail/Parse/Parsers/Header.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Parsers/Header.hs
@@ -0,0 +1,36 @@
+module Network.Mail.Parse.Parsers.Header where
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Utils
+import Network.Mail.Parse.Parsers.HeaderFields (parseText)
+
+import Data.Word8
+import Data.Attoparsec.ByteString
+import qualified Data.Attoparsec.ByteString as AP
+import Data.Text.Encoding (decodeUtf8)
+import Data.Either.Unwrap (fromRight)
+import Data.Either (isRight)
+
+import qualified Data.ByteString.Char8 as BSC
+
+-- |Parses a header
+headerParser :: Parser Header
+headerParser = do
+  headerName <- AP.takeWhile (/= _colon)
+  word8 _colon
+  AP.takeWhile isWhitespace
+
+  headerLine <- consumeTillEndLine
+  moreLines <- many' isConsequentHeaderLine
+
+  let headerBody = decodeUtf8 . cleanupLines $ headerLine:moreLines
+  let parsedBody = parseText headerBody
+  let body = if isRight parsedBody then fromRight parsedBody else headerBody
+
+  return $ Header (decodeUtf8 headerName) body
+
+-- |Concatenate lines insterting whitespace between them.
+-- The whitespace needs to be inserted as these lines
+-- come from parser that eats up the whitespace
+cleanupLines :: [BSC.ByteString] -> BSC.ByteString
+cleanupLines ls = BSC.intercalate " " $ map BSC.init ls
diff --git a/src/Network/Mail/Parse/Parsers/HeaderFields.hs b/src/Network/Mail/Parse/Parsers/HeaderFields.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Parsers/HeaderFields.hs
@@ -0,0 +1,207 @@
+module Network.Mail.Parse.Parsers.HeaderFields (
+  emailAddressParser,
+  emailAddressListParser,
+  parseTime,
+  parseEmailAddress,
+  parseEmailAddressList,
+  parseText,
+  parseTextList,
+  parseMessageId
+) where
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Decoders.BodyDecoder (transferDecode, encodingToUtf)
+
+import Data.Attoparsec.Text
+import qualified Data.Text as T
+import qualified Data.Text.Read as TR
+import qualified Data.Attoparsec.Text as AP
+import Data.Text.Encoding (encodeUtf8)
+import Control.Applicative
+import Data.Maybe
+import qualified Data.Char as C
+import Data.Either (isRight)
+import Data.Either.Combinators (mapLeft, mapBoth)
+
+import Data.Either.Unwrap (fromRight)
+
+import Data.Time.Parse (strptime)
+import Data.Time.LocalTime
+import Control.Monad (join, liftM)
+
+-- |Parses a name-addr formatted email
+nameAddrParser :: Parser EmailAddress
+nameAddrParser = do
+  label <- AP.takeWhile (/= '<')
+  _ <- char '<'
+
+  address <- AP.takeWhile1 (/= '>')
+  _ <- char '>'
+  return $ EmailAddress address (Just . T.strip $ label)
+
+-- |Parses an addr-spec formatted email
+addrSpecParser :: Parser EmailAddress
+addrSpecParser = do
+  address <- AP.takeWhile1 (\c -> c /= '\r' && c /= ',' && c /= ' ')
+  if isJust $ T.find (== '@') address
+    then return $ EmailAddress address Nothing
+    else fail "no @ in the address"
+
+-- |Parses an email in any format
+emailAddressParser :: Parser EmailAddress
+emailAddressParser = nameAddrParser <|> addrSpecParser
+
+-- |Eats unneeded whitespace
+eatWhitespace :: Parser T.Text
+eatWhitespace = AP.takeWhile (\c -> c == ',' || c == ' ')
+
+-- |Parses a list of email addresses
+emailAddressListParser :: Parser [EmailAddress]
+emailAddressListParser =
+  (eatWhitespace *> emailAddressParser) `sepBy'` char ','
+
+messageIdParser :: Parser MessageId
+messageIdParser = parseWrappedMsgId <|> takeText
+
+parseWrappedMsgId :: Parser MessageId
+parseWrappedMsgId = do
+  _ <- char '<'
+  msgId <- AP.takeWhile1 (/= '>')
+  _ <- char '>'
+  return msgId
+
+minutesAndHoursToTZ :: Int -> Either T.Text (Int, T.Text) ->
+  (Int, T.Text) -> Either T.Text TimeZone
+minutesAndHoursToTZ direction minutes hours =
+    Right $ minutesToTimeZone timezoneMins
+  where knownMinutes  = if isRight minutes
+                          then fst . fromRight $ minutes
+                          else 0
+        h             = fst hours
+        timezoneMins  = direction * (h * 60 + knownMinutes)
+
+zoneToOffset :: T.Text -> Either ErrorMessage TimeZone
+zoneToOffset offset = if offsetH == '+' || offsetH == '-'
+  then hours >>= (minutesAndHoursToTZ direction minutes)
+  else Right $ minutesToTimeZone . (*60) $ case offset of
+    "UT" -> 0
+    "GMT" -> 0
+    "EST" -> -5
+    "EDT" -> -4
+    "CST" -> -6
+    "CDT" -> -5
+    "MST" -> -7
+    "MDT" -> -6
+    "PST" -> -8
+    "PDT" -> -7
+    _ -> 0
+  where offsetH = T.head offset
+        direction = if offsetH == '+' then 1 else -1
+        splitOffset = T.splitAt 2 $ T.tail offset
+        hours = mapLeft T.pack $ TR.decimal . fst $ splitOffset
+        minutes = mapLeft T.pack $ TR.decimal . snd $ splitOffset
+
+timeToLocalTime :: (T.Text, T.Text, T.Text, [T.Text]) -> Maybe LocalTime
+timeToLocalTime (day, month, year, timeOfDay@(hours:minutes:_)) =
+    liftM fst (strptime "%d %b %Y %T" dateString)
+  where dateString = T.intercalate " " [day, month, year, timeString]
+        seconds = if length timeOfDay < 3 then "0" else last timeOfDay
+        timeString = T.intercalate ":" [hours, minutes, seconds]
+
+timeParser :: Parser (Either ErrorMessage ZonedTime)
+timeParser =
+  do
+    day <- AP.takeWhile1 C.isDigit
+    AP.takeWhile (== ' ')
+
+    month <- AP.takeWhile1 C.isLetter
+    AP.takeWhile (== ' ')
+
+    year <- AP.takeWhile1 C.isDigit
+    AP.takeWhile (== ' ')
+
+    timeOfDay <- (AP.takeWhile (== ':') *> AP.takeWhile1 C.isDigit) `sepBy` char ':'
+
+    AP.takeWhile (== ' ')
+    zone <- AP.takeWhile1 (/= ' ')
+    let localTime = timeToLocalTime (day, month, year, timeOfDay)
+    let timeZone = zoneToOffset zone
+    let result = if isJust localTime && isRight timeZone
+                  then Right $ ZonedTime (fromJust localTime) (fromRight timeZone)
+                  else mapBoth (const "cannot decode timezone") (const defaultZT) timeZone
+    return result
+
+-- |Parse a time from a header containing time
+parseTime :: T.Text -> Either ErrorMessage ZonedTime
+parseTime dateString = join . mapLeft T.pack $ parseOnly timeParser withoutDoW
+  where withoutDoW = T.strip . last $  T.splitOn "," dateString
+
+parseMessageId :: T.Text -> Either ErrorMessage MessageId
+parseMessageId = mapLeft T.pack . parseOnly messageIdParser
+
+parseEmailAddress :: T.Text -> Either ErrorMessage EmailAddress
+parseEmailAddress = mapLeft T.pack . parseOnly emailAddressParser
+
+parseEmailAddressList :: T.Text -> Either ErrorMessage [EmailAddress]
+parseEmailAddressList = mapLeft T.pack . parseOnly emailAddressListParser
+
+untilEndSection :: Char -> Char -> Maybe Char
+untilEndSection prev current =
+  if prev == '?' && current == '='
+    then Nothing
+    else Just current
+
+untilStartSection :: Char -> Char -> Maybe Char
+untilStartSection prev current =
+  if prev == '=' && current == '?'
+    then Nothing
+    else Just current
+
+parseInlineEncoding :: Parser (Either ErrorMessage T.Text)
+parseInlineEncoding = do
+  charset <- AP.takeWhile1 (/= '?')
+  char '?'
+
+  encoding <- AP.takeWhile1 (/= '?')
+  char '?'
+
+  matchedText <- AP.scan ' ' untilEndSection
+  char '='
+
+  let text = encodeUtf8 $ T.init matchedText
+  let decoded = mapLeft (const "Count not decode encoding") (transferDecode text encoding) >>= return . (`encodingToUtf` charset)
+
+  if T.toLower encoding == "q"
+    then return $ liftM (T.replace "_" " ") decoded
+    else return decoded
+
+parseTextBlock :: Parser (Either ErrorMessage T.Text)
+parseTextBlock = do
+  before <- AP.scan ' ' untilStartSection
+  endReached <- atEnd
+  decoded <- if not endReached
+    then char '?' >> parseInlineEncoding
+    else return . Right $ T.empty
+
+  let didDecode = isRight decoded && (not . T.null . fromRight $ decoded)
+  let normalizedBefore = if didDecode then T.init before else before
+  return $ liftM (T.append normalizedBefore) decoded
+
+untilEOF :: Parser (Either ErrorMessage T.Text) -> Parser [Either ErrorMessage T.Text]
+untilEOF parser = do
+  parsed <- parser
+  endReached <- atEnd
+  if endReached
+    then return [parsed]
+    else liftM (parsed:) (untilEOF parser)
+
+parseText' :: Parser (Either ErrorMessage T.Text)
+parseText' = do
+  blocks <- untilEOF parseTextBlock
+  return $ liftM T.concat (mapM id blocks)
+
+parseText :: T.Text -> Either ErrorMessage T.Text
+parseText = join . mapLeft T.pack . parseOnly parseText'
+
+parseTextList :: T.Text -> T.Text -> Either ErrorMessage [T.Text]
+parseTextList splitChar t = mapM parseText $ T.splitOn splitChar t
diff --git a/src/Network/Mail/Parse/Parsers/Message.hs b/src/Network/Mail/Parse/Parsers/Message.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Parsers/Message.hs
@@ -0,0 +1,107 @@
+module Network.Mail.Parse.Parsers.Message (messageParser) where
+
+import Data.Attoparsec.ByteString
+
+import Data.List (find)
+import Data.Maybe
+import Data.Either (isRight)
+import Control.Monad (liftM)
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Utils
+
+import Network.Mail.Parse.Parsers.Utils (isMIME, discoverAttachment)
+import Network.Mail.Parse.Parsers.Multipart (parseMultipart)
+import Network.Mail.Parse.Decoders.BodyDecoder (decodeBody, decodeTextBody)
+import Network.Mail.Parse.Parsers.Header (headerParser)
+import Network.Mail.Parse.Parsers.HeaderFields
+import qualified Data.Text as T
+
+import qualified Data.ByteString.Char8 as BSC
+import Data.Either.Utils (maybeToEither)
+
+import Data.Text.Encoding (encodeUtf8)
+import Data.Either.Combinators (fromRight', fromRight, mapLeft)
+
+import Codec.MIME.Parse (parseMIMEType)
+import Codec.MIME.Type
+
+import Control.Monad (join)
+
+parseHeader :: Header -> Header
+parseHeader header = fromRight header parsedHeader
+  where hname        = headerName header
+        contents     = headerContents header
+        references   = parseTextList " " contents >>= mapM parseMessageId
+        parsedHeader = case T.toLower hname of
+          "date" -> Date <$> parseTime contents
+          "from" -> From <$> parseEmailAddress contents
+          "reply-to" -> ReplyTo <$> parseEmailAddress contents
+          "to" -> To <$> parseEmailAddressList contents
+          "cc" -> CC <$> parseEmailAddressList contents
+          "bcc" -> BCC <$> parseEmailAddressList contents
+          "message-id" -> MessageId <$> parseMessageId contents
+          "in-reply-to" -> InReplyTo <$> parseMessageId contents
+          "references" -> References <$> references
+          "subject" -> Right $ Subject contents
+          "comments" -> Right $ Comments contents
+          "keywords" -> Keywords <$> parseTextList "," contents
+          _ -> Right header
+
+-- |Parses a single message
+messageParser :: Maybe [Header] -> -- ^ Headers, if they were already parsed
+                Maybe [Header] -> -- ^ Context headers, useful is encoding is only
+                                  -- defined in the message above, for instance
+                Parser (Either ErrorMessage EmailMessage)
+messageParser headersIn helperHeadersIn = do
+  headers <- if isJust headersIn
+    then return . fromJust $ headersIn
+    else manyTill' headerParser $ string "\r\n"
+  let helperHeaders = if isJust helperHeadersIn then fromJust helperHeadersIn else []
+
+  body <- takeByteString
+  let parsedHeaders = map parseHeader headers
+
+  -- Parse MIME if the message is in a MIME format
+  let parsedBody = if isJust $ find isMIME headers
+      then parseMIME (headers ++ helperHeaders) body
+      else Right [TextBody $ decodeTextBody (headers ++ helperHeaders) body]
+
+  return $! parsedBody >>= return . EmailMessage parsedHeaders
+
+
+-- |Parses a MIME message part. Needs headers from the actual message
+-- in case the MIME block misses some encoding blocks
+mimeParser :: [Header] -> Parser (Either ErrorMessage EmailBody)
+mimeParser bodyHeaders = do
+  headers <- manyTill' headerParser $ string "\r\n"
+  let isAttachment = discoverAttachment headers
+  if isJust isAttachment
+    then do
+      body <- takeByteString
+      let filename = fromJust isAttachment
+      let decodedBody = decodeBody headers body
+      return . Right $ Attachment headers filename (Just decodedBody) Nothing
+    else (liftM . liftM) MessageBody $ messageParser (Just headers) (Just bodyHeaders)
+
+-- |Parse a set of parts.
+multipartParser :: [Header] -> [BSC.ByteString] -> Either ErrorMessage [EmailBody]
+multipartParser bodyHeaders parts = do
+  mapM (\p -> join $ mapLeft T.pack $ parseOnly (mimeParser bodyHeaders) p) parts
+
+-- |Parse a mime encoded body.
+parseMIME :: [Header] -> BSC.ByteString -> Either ErrorMessage [EmailBody]
+parseMIME headers body = if isRight msgType then
+  (case mimeType . fromRight' $ msgType of
+    Multipart _ -> multiParsed >>= multipartParser headers
+    Text _ -> Right decodedBody
+    _ -> Left "mimetype not supported")
+  else Right decodedBody
+  where msgType = findHeader "Content-Type" headers >>=
+          Right . parseMIMEType . headerContents >>=
+          maybeToEither "Couldn't parse message type"
+        multiParsed = msgType >>=
+          \x -> maybeToEither "" $ find (\p -> paramName p == "boundary") (mimeParams x) >>=
+          return . encodeUtf8 . paramValue >>=
+          \b -> eitherToMaybe $ parseOnly (parseMultipart b) body
+        decodedBody = [TextBody $ decodeTextBody headers body]
diff --git a/src/Network/Mail/Parse/Parsers/Multipart.hs b/src/Network/Mail/Parse/Parsers/Multipart.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Parsers/Multipart.hs
@@ -0,0 +1,31 @@
+module Network.Mail.Parse.Parsers.Multipart (parseMultipart) where
+
+import qualified Data.ByteString.Char8 as BSC
+import qualified Data.ByteString as BS
+import Data.Word8
+import Data.Attoparsec.ByteString
+
+import Network.Mail.Parse.Utils
+
+isBoundaryMatched :: BSC.ByteString -> Int -> Word8 -> Maybe Int
+isBoundaryMatched boundary matchIdx char =
+  if char == BS.index boundary matchIdx
+    then if matchIdx == boundaryLength - 1
+          then Nothing
+          else Just $ matchIdx + 1
+    else Just 0
+  where boundaryLength = BS.length boundary
+
+trimPayload :: BSC.ByteString -> BSC.ByteString -> BSC.ByteString
+trimPayload boundary payload = BSC.take trimLength payload
+  where payloadLength  = BSC.length payload
+        boundaryLength = BSC.length boundary
+        trimLength     = payloadLength - boundaryLength + 1
+
+parseMultipart :: BSC.ByteString -> Parser [BSC.ByteString]
+parseMultipart boundary =
+  do
+    _ <- manyTill' anyWord8 (string completeBoundary) <* consumeTillEndLine
+    payloads <- many' (scan 0 (isBoundaryMatched completeBoundary) <* consumeTillEndLine)
+    return $ map (trimPayload completeBoundary) payloads
+  where completeBoundary = BSC.append "--" boundary
diff --git a/src/Network/Mail/Parse/Parsers/Utils.hs b/src/Network/Mail/Parse/Parsers/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Parsers/Utils.hs
@@ -0,0 +1,36 @@
+module Network.Mail.Parse.Parsers.Utils where
+
+import Network.Mail.Parse.Types
+import Network.Mail.Parse.Utils
+
+import qualified Data.Text as T
+import Data.List
+import Control.Monad (liftM)
+
+
+-- |Check if the given headers represent an attachment
+discoverAttachment :: [Header] -> Maybe T.Text
+discoverAttachment headers = hdr >>= findAttachmentName . headerContents
+  where hdr = find (\x -> (T.toLower . headerName $ x) == "content-disposition") headers
+
+-- |When provided with a content-disposition header,
+-- checks if it represents an attachment. If it does it returns
+-- it's name, otherwise Nothing
+findAttachmentName :: T.Text -> Maybe T.Text
+findAttachmentName header =
+  if dispType == "attachment" || dispType == "inline"
+    then if length split == 1
+      then Just ""
+      else liftM (T.strip . T.dropAround (== '"') . (!! 1)) filenameParam
+    else Nothing
+  where split = T.splitOn ";" header
+        dispType = T.toLower . T.strip . head $ split
+        paramSplit = map (T.splitOn "=") (tail split)
+        filenameParam = find (\x -> T.strip (head x) == "filename") paramSplit
+
+-- |Decide if the header contains a valid MIME info
+isMIME :: Header -> Bool
+isMIME header = isNameValid && isVersionValid
+  where isNameValid    = T.toLower (headerName header) == "mime-version"
+        version        = commentRemover $ headerContents header
+        isVersionValid = version == "1.0"
diff --git a/src/Network/Mail/Parse/Types.hs b/src/Network/Mail/Parse/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Types.hs
@@ -0,0 +1,84 @@
+module Network.Mail.Parse.Types where
+
+import Data.Text
+import GHC.Generics
+import qualified Data.ByteString.Char8 as BS
+import Data.Time.Calendar (Day(..))
+import Data.Time.LocalTime (ZonedTime(..), minutesToTimeZone, LocalTime(..),
+  TimeOfDay(..))
+
+type UID = Integer
+type ErrorMessage = Text
+
+data EmailMessage = EmailMessage {
+  emailHeaders :: [Header],
+  emailBodies :: ![EmailBody]
+} deriving (Show, Eq, Generic)
+
+type MessageId = Text
+data Header = Date ZonedTime
+            | From EmailAddress
+            | ReplyTo EmailAddress
+            | To [EmailAddress]
+            | CC [EmailAddress]
+            | BCC [EmailAddress]
+            | MessageId MessageId
+            | InReplyTo MessageId
+            | References [MessageId]
+            | Subject Text
+            | Comments Text
+            | Keywords [Text]
+            | Header {
+                headerName :: !Text,
+                headerContents :: !Text
+            }
+            deriving (Eq, Show, Generic)
+
+data EmailAddress = EmailAddress {
+  emailAddress :: !Text,
+  emailLabel :: Maybe Text
+} deriving (Show, Generic)
+
+instance Eq EmailAddress where
+  x == y = emailAddress x == emailAddress y
+
+-- |An email body contains the contents of an email part
+-- up until the boundary marker.
+data EmailBody
+  -- |Body of a MIME message part. Contains headers
+  = MessageBody EmailMessage
+  -- = MIMEBody { mimeHeaders :: ![Header], mimeBody :: !Text}
+  -- |If the message contained no MIME information, it's probably
+  -- just some text. Best guess decoding into UTF-8 is applied
+  | TextBody !Text
+  -- |Attachment is part of a MIME message, but a rather special
+  -- one. It's decoded from whatever the transfer encoding was applied
+  -- and left as a raw sollection of bytes for your enjoyment
+  | Attachment {
+    attachmentHeaders :: ![Header],
+    attachmentName :: !Text,
+    attachmentBody :: Maybe BS.ByteString,
+    -- |Location of the actual filename on disk
+    storageFilename :: Maybe Text
+  }
+  deriving (Eq, Show, Generic)
+
+
+instance Eq ZonedTime where
+  x == y = (zonedTimeToLocalTime x) == (zonedTimeToLocalTime y) &&
+           (zonedTimeZone x) == (zonedTimeZone y)
+
+defaultZT :: ZonedTime
+defaultZT = ZonedTime {
+  zonedTimeZone = minutesToTimeZone 0,
+  zonedTimeToLocalTime = LocalTime {
+    localDay = ModifiedJulianDay {
+      toModifiedJulianDay = 1
+    },
+    localTimeOfDay = TimeOfDay {
+      todHour = 0,
+      todMin = 0,
+      todSec = 0
+    }
+  }
+}
diff --git a/src/Network/Mail/Parse/Utils.hs b/src/Network/Mail/Parse/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Mail/Parse/Utils.hs
@@ -0,0 +1,56 @@
+module Network.Mail.Parse.Utils where
+
+import Network.Mail.Parse.Types
+
+import Data.Attoparsec.ByteString
+import qualified Data.Attoparsec.ByteString as AP
+import qualified Data.ByteString.Char8 as BS
+import Data.Word8
+import Data.List
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Either.Utils (maybeToEither)
+
+-- |If the previous character was a carriage return and the current
+-- is a line feed, stop parsing
+hadCRLF :: Word8 -> Word8 -> Maybe Word8
+hadCRLF prev current =
+  if prev == _cr && current == _lf
+    then Nothing
+    else Just current
+
+-- |Consumes a line until CRLF is hit
+consumeTillEndLine :: Parser BS.ByteString
+consumeTillEndLine = scan 0 hadCRLF <* satisfy (== _lf)
+
+-- |Can a given character be regarded as a whitespace?
+isWhitespace :: Word8 -> Bool
+isWhitespace x = x == 9 || x == 32
+
+-- |If the next line is a part of a previous header, parse it.
+-- Fail otherwise
+isConsequentHeaderLine :: Parser BS.ByteString
+isConsequentHeaderLine = satisfy isWhitespace *>
+                         AP.takeWhile isWhitespace *>
+                         consumeTillEndLine
+
+-- |Remove a MIME header comment and return a header without the comment
+commentRemover :: Text -> Text
+commentRemover contents = T.strip withoutComment
+  where splitAtComment = T.split (\c -> c == '(' || c == ')') contents
+        withoutComment = if length splitAtComment > 1
+                          then T.append (head splitAtComment) (last splitAtComment)
+                          else head splitAtComment
+
+-- |Given a header name, it will try to locate it in
+-- a list of headers, fail if it's not there
+findHeader :: Text -> [Header] -> Either ErrorMessage Header
+findHeader hdr headers = maybeToEither notFound header
+  where notFound    = T.concat ["Cound not find header '", T.pack . show $ hdr, "'"]
+        eigenHeader = T.toLower hdr
+        header      = find (\x -> T.toLower (headerName x) == eigenHeader) headers
+
+eitherToMaybe :: Either a b -> Maybe b
+eitherToMaybe (Right a) = Just a
+eitherToMaybe (Left _) = Nothing
diff --git a/test/main.hs b/test/main.hs
new file mode 100644
--- /dev/null
+++ b/test/main.hs
@@ -0,0 +1,9 @@
+module Main where
+
+import Test.Tasty (defaultMain, testGroup)
+import qualified Network.Mail.Parse.Parsers.Tests.Headers as TH
+import qualified Network.Mail.Parse.Parsers.Tests.HeaderFields as THF
+
+main :: IO ()
+main = defaultMain $ testGroup "Tests"
+  [TH.tests, THF.tests]
