Skip to content

Commit

Permalink
Merge branch 'wireapp:develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
offsoc authored Oct 23, 2024
2 parents 0fd47d7 + 6f96123 commit 34e46bf
Show file tree
Hide file tree
Showing 30 changed files with 670 additions and 278 deletions.
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ cabal.project.local:
c: treefmt c-fast

.PHONY: c
c-fast:
c-fast:
cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false )
ifeq ($(test), 1)
./hack/bin/cabal-run-tests.sh $(package) $(testargs)
Expand Down Expand Up @@ -173,7 +173,7 @@ lint-all: formatc hlint-check-all lint-common
# The extra 'hlint-check-pr' has been witnessed to be necessary due to
# some bu in `hlint-inplace-pr`. Details got lost in history.
.PHONY: lint-all-shallow
lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr
lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr

.PHONY: lint-common
lint-common: check-local-nix-derivations treefmt-check # weeder (does not work on CI yet)
Expand Down Expand Up @@ -602,3 +602,8 @@ upload-bombon:
--project-version $(HELM_SEMVER) \
--api-key $(DEPENDENCY_TRACK_API_KEY) \
--auto-create

.PHONY: openapi-validate
openapi-validate:
@echo -e "Make sure you are running the backend in another terminal (make cr)\n"
vacuum lint -a -d -w <(curl http://localhost:8082/v7/api/swagger.json)
1 change: 1 addition & 0 deletions changelog.d/3-bug-fixes/ses-notifications
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Process bounce and complaint notifications from SES correctly.
1 change: 1 addition & 0 deletions changelog.d/4-docs/openapi-validation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix openapi validation errors
2 changes: 2 additions & 0 deletions libs/types-common/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ mkDerivation {
bytestring
bytestring-conversion
cereal
email-validate
imports
protobuf
string-conversions
Expand All @@ -132,6 +133,7 @@ mkDerivation {
text
time
unordered-containers
utf8-string
uuid
];
description = "Shared type definitions";
Expand Down
214 changes: 214 additions & 0 deletions libs/types-common/src/Data/Mailbox.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
module Data.Mailbox where

import Control.Applicative (optional)
import Data.Aeson
import Data.Attoparsec.ByteString (Parser)
import Data.Attoparsec.ByteString qualified as BSParser
import Data.Attoparsec.ByteString.Char8 qualified as Char8Parser
import Data.Char qualified as Char
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Imports
import Text.Email.Parser

-- | Mailbox address according to
-- https://www.rfc-editor.org/rfc/rfc5322#section-3.4
data Mailbox = Mailbox
{ name :: Maybe [Text],
address :: EmailAddress
}
deriving (Show, Eq)

parseMailbox :: ByteString -> Either String Mailbox
parseMailbox = BSParser.parseOnly (mailboxParser <* BSParser.endOfInput)

instance FromJSON Mailbox where
parseJSON =
withText "Mailbox" $
either fail pure . parseMailbox . Text.encodeUtf8

-- * Internal

newtype Comment = Comment [CommentContent]

data CommentContent = CommentChar Char | SubComment Comment

atextParser :: Parser Char
atextParser =
alpha
<|> num
<|> allowedSpecials
where
alpha = Char8Parser.satisfy (\c -> Char.isAlpha c && Char.isAscii c)
num = Char8Parser.satisfy Char.isNumber
allowedSpecials =
Char8Parser.satisfy $
-- Make sure the - is the first or the last symbol, otherwise inClass
-- treats it as a signifier of range
Char8Parser.inClass "-!#$%&'*+/=?^_`{|}~"

wspParser :: Parser Char
wspParser = Char8Parser.satisfy (\c -> c == ' ' || c == '\t')

crlfParser :: Parser String
crlfParser = do
void $ Char8Parser.string "\r\n"
pure "\r\n"

fwsParser :: Parser String
fwsParser =
let wspsAndCrlf = do
wsps <- Char8Parser.many' wspParser
crlf <- crlfParser
pure $ wsps <> crlf
notObs = do
mWspsAndCrlf <- optional wspsAndCrlf
wsps <- Char8Parser.many1' wspParser
pure $ fromMaybe "" mWspsAndCrlf <> wsps
in notObs <|> obsFwsParser

obsFwsParser :: Parser String
obsFwsParser = do
wsps <- Char8Parser.many1' wspParser
crlfWsps <- Char8Parser.many' $ do
crlf <- crlfParser
wspsAfterCrlf <- Char8Parser.many1' wspParser
pure $ crlf <> wspsAfterCrlf
pure $ concat $ wsps : crlfWsps

ctextParser :: Parser Char
ctextParser = do
let isAllowedChar w =
(w >= 33 && w <= 39)
|| (w >= 42 && w <= 91)
|| (w >= 93 && w <= 126)
Char8Parser.satisfy (isAllowedChar . Char.ord) <|> obsNoWsCtl

-- | US-ASCII control characters that do not include the carriage return, line
-- feed, and white space characters
obsNoWsCtl :: Parser Char
obsNoWsCtl = do
Char8Parser.satisfy
( \(ord -> c) ->
(c >= 1 && c <= 8)
|| c == 11
|| c == 12
|| (c >= 14 && c <= 31)
|| (c == 127)
)

obsCtextParser, obsQtextParser :: Parser Char
obsCtextParser = obsNoWsCtl
obsQtextParser = obsNoWsCtl

quotedPairParser :: Parser Char
quotedPairParser = do
void $ Char8Parser.char '\\'
vCharParser <|> wspParser

vCharParser :: Parser Char
vCharParser =
Char8Parser.satisfy (\c -> ord c >= 0x21 && ord c <= 0x7E)

ccontentParser :: Parser CommentContent
ccontentParser =
fmap CommentChar ctextParser
<|> fmap CommentChar quotedPairParser
<|> fmap SubComment commentParser

commentParser :: Parser Comment
commentParser = do
_ <- Char8Parser.char '('
comment <- Char8Parser.many' $ do
_ <- optional fwsParser
ccontentParser
_ <- Char8Parser.char ')'
pure $ Comment comment

cfwsParser :: Parser [Comment]
cfwsParser = do
let commentWithFws = do
comment <- Char8Parser.many1' $ do
_ <- optional fwsParser
commentParser
_ <- optional fwsParser
pure comment
commentWithFws <|> fmap (const []) fwsParser

atomParser :: Parser String
atomParser = do
_ <- optional cfwsParser
atom <- Char8Parser.many1' atextParser
_ <- optional cfwsParser
pure atom

qtextParser :: Parser Char
qtextParser =
let newParser = Char8Parser.satisfy $ \(ord -> c) ->
c == 33 || (c >= 35 && c <= 91) || (c >= 93 && c <= 126)
in newParser <|> obsQtextParser

qcontentParser :: Parser Char
qcontentParser = qtextParser <|> quotedPairParser

quotedStringParser :: Parser String
quotedStringParser = do
_ <- optional cfwsParser
_ <- Char8Parser.char '"'
str <- fmap concat . Char8Parser.many' $ do
mLeadingSpace <- optional fwsParser
c <- qcontentParser
pure $ fromMaybe "" mLeadingSpace <> [c]
mTrailingSpace <- optional fwsParser
_ <- Char8Parser.char '"'
pure $ str <> fromMaybe "" mTrailingSpace

wordParser :: Parser String
wordParser = atomParser <|> quotedStringParser

-- | The spec says
--
-- @
-- phrase = 1*word / obs-phrase
-- @
--
-- Here if we tried to write it using '<|>', parising "John Q. Doe" would
-- succeed with a 'many1 wordParser' while having parsed up to "John Q" and the
-- rest of the string will be left for next parsers, which would likely fail. To
-- avoid all that we can use just the obsPhraseParser, which forces the first
-- thing to be a word and then allows for dots and CFWS.
phraseParser :: Parser [String]
phraseParser = obsPhraseParser

-- | Ignores comments
obsPhraseParser :: Parser [String]
obsPhraseParser = do
w1 <- wordParser
ws <- fmap catMaybes . Char8Parser.many' $ do
fmap Just wordParser
<|> fmap (Just . (: [])) (Char8Parser.char '.')
<|> fmap (const Nothing) cfwsParser
pure $ w1 : ws

nameParser :: Parser [Text]
nameParser = map Text.pack <$> phraseParser

-- | Does not implement parsing for obs-angle-addr
angleAddrParser :: Parser EmailAddress
angleAddrParser = do
_ <- optional cfwsParser
_ <- Char8Parser.char '<'
addr <- addrSpec
_ <- Char8Parser.char '>'
_ <- optional cfwsParser
pure addr

nameAddrParser :: Parser Mailbox
nameAddrParser =
Mailbox
<$> optional nameParser
<*> angleAddrParser

mailboxParser :: Parser Mailbox
mailboxParser =
nameAddrParser <|> fmap (Mailbox Nothing) addrSpec
12 changes: 6 additions & 6 deletions libs/types-common/src/Data/Range.hs
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,20 @@ instance (ToParamSchema a, KnownNat n, KnownNat m) => ToParamSchema (Range n m [
instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m String) where
toParamSchema _ =
toParamSchema (Proxy @String)
& S.maxLength ?~ fromKnownNat (Proxy @n)
& S.minLength ?~ fromKnownNat (Proxy @m)
& S.minLength ?~ fromKnownNat (Proxy @n)
& S.maxLength ?~ fromKnownNat (Proxy @m)

instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m T.Text) where
toParamSchema _ =
toParamSchema (Proxy @T.Text)
& S.maxLength ?~ fromKnownNat (Proxy @n)
& S.minLength ?~ fromKnownNat (Proxy @m)
& S.minLength ?~ fromKnownNat (Proxy @n)
& S.maxLength ?~ fromKnownNat (Proxy @m)

instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where
toParamSchema _ =
toParamSchema (Proxy @TL.Text)
& S.maxLength ?~ fromKnownNat (Proxy @n)
& S.minLength ?~ fromKnownNat (Proxy @m)
& S.minLength ?~ fromKnownNat (Proxy @n)
& S.maxLength ?~ fromKnownNat (Proxy @m)

instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where
declareNamedSchema _ =
Expand Down
4 changes: 3 additions & 1 deletion libs/types-common/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Main
where

import Imports
import Test.Data.Mailbox qualified as Mailbox
import Test.Data.PEMKeys qualified as PEMKeys
import Test.Domain qualified as Domain
import Test.Handle qualified as Handle
Expand All @@ -39,5 +40,6 @@ main =
Domain.tests,
Handle.tests,
Qualified.tests,
PEMKeys.tests
PEMKeys.tests,
Mailbox.tests
]
69 changes: 69 additions & 0 deletions libs/types-common/test/Test/Data/Mailbox.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Test.Data.Mailbox (tests) where

import Data.ByteString.UTF8 qualified as UTF8BS
import Data.Mailbox
import Imports
import Test.Tasty
import Test.Tasty.HUnit
import Text.Email.Parser

validAddresses :: [(ByteString, Mailbox)]
validAddresses =
[ ("[email protected]", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"),
("<[email protected]>", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"),
("John Doe<[email protected]>", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("John Doe <[email protected]>", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("John Q. Doe <[email protected]>", Mailbox (Just ["John", "Q", ".", "Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" <[email protected]>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" (My Best Friend) <[email protected]>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"[email protected]\" (My Friend @ Doe) <[email protected]>", Mailbox (Just ["[email protected]"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" (My Best Friend) <[email protected]>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John \\\"The J\\\" Doe\" <[email protected]>", Mailbox (Just ["John \"The J\" Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John not \\tab\" <[email protected]>", Mailbox (Just ["John not tab"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John [Quoted Special]\" <[email protected]>", Mailbox (Just ["John [Quoted Special]"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John <[email protected]>\" <[email protected]>", Mailbox (Just ["John <[email protected]>"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John \r\n NewLine\" <[email protected]>", Mailbox (Just ["John \r\n NewLine"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" <(local comment)john(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" <(local comment)\"john\"(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john\"" "doe.example"),
("\"John Doe\" <\"john@funkylocal\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john@funkylocal\"" "doe.example"),
("\"John Doe\" <[email protected]> (trailing comments)", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"),
("\"John Doe\" <john@[[email protected]]>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[[email protected]]"),
("\"John Doe\" <john@(domain comment)[doe.example](trailing domain comment)>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[doe.example]"),
-- This is wrong, but its how the `email-validate` library does it
("\"John Doe\" <\"john (not comment)\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john(notcomment)\"" "doe.example")
]

invalidAddresses :: [ByteString]
invalidAddresses =
[ "john",
"john@",
"@doe.example",
"\"[email protected]\"",
"([email protected])",
"\"John UnendingQuote <[email protected]>",
"John [Unquoted Special] <[email protected]>",
"<[email protected]> <[email protected]>",
"\"John \n NoCR\" <[email protected]>",
"\"John \r NoLF\" <[email protected]>"
]

tests :: TestTree
tests =
testGroup "Mailbox" $
[ testGroup "valid addressses" $
map
( \(addr, expected) ->
testCase (UTF8BS.toString addr) $
Right expected @=? parseMailbox addr
)
validAddresses,
testGroup "invalid addresses" $
map
( \addr ->
testCase (UTF8BS.toString addr) $
case parseMailbox addr of
Left _ -> pure ()
Right mb -> assertFailure $ "Expected to fail parising, but got: " <> show mb
)
invalidAddresses
]
Loading

0 comments on commit 34e46bf

Please sign in to comment.