Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
unreleased
- added IMAP UIDPLUS support (RFC 4315): appendFullUID, copyUID, copyUIDs,
copyUIDR, uidExpunge, uidExpungeR, plus the APPENDUID/COPYUID/UIDNOTSTICKY
response codes and AppendUID/CopyUID types
- IMAP response parser is now case-insensitive for protocol keywords,
status codes and flag names (per RFC 3501); also surfaces untagged BYE as
a fatal response, accepts an empty SEARCH reply and a NIL LIST hierarchy
separator, and no longer lets atoms run past CRLF

0.7 (2025-06-08)
- updated to the lastest GHC, tested with 8.10.7 — 9.12.2 (PR #103)
Thanks to @thomasjm
Expand Down
78 changes: 71 additions & 7 deletions src/Network/HaskellNet/IMAP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
-- ** autenticated state commands
, select, examine, create, delete, rename
, subscribe, unsubscribe
, list, lsub, status, append, appendFull
, list, lsub, status, append, appendFull, appendFullUID
-- ** selected state commands
, check, close, expunge
, search, store, copy, move
, search, store, copy, copyUID, copyUIDs, copyUIDR, uidExpunge, uidExpungeR, move
, idle
-- * fetch commands
, fetch, fetchHeader, fetchSize, fetchHeaderFields, fetchHeaderFieldsNot
Expand All @@ -20,6 +20,7 @@
, fetchPeek, fetchRPeek
-- * other types
, Flag(..), Attribute(..), MailboxStatus(..)
, AppendUID(..), CopyUID(..), UIDSet
, SearchQuery(..), FlagsQuery(..)
, A.AuthType(..)
)
Expand Down Expand Up @@ -164,12 +165,19 @@
sendCommand :: IMAPConnection -> String
-> (RespDerivs -> Result RespDerivs (ServerResponse, MboxUpdate, v))
-> IO v
sendCommand imapc cmdstr pFunc =
sendCommand imapc cmdstr pFunc = snd <$> sendCommandWithResponse imapc cmdstr pFunc

-- | Like 'sendCommand', but also returns the tagged 'ServerResponse' so callers
-- can inspect response codes (e.g. the UIDPLUS @COPYUID@/@APPENDUID@ codes).
sendCommandWithResponse :: IMAPConnection -> String
-> (RespDerivs -> Result RespDerivs (ServerResponse, MboxUpdate, v))
-> IO (ServerResponse, v)
sendCommandWithResponse imapc cmdstr pFunc =
do (buf, num) <- sendCommand' imapc cmdstr
let (resp, mboxUp, value) = eval pFunc (show6 num) buf
case resp of
OK _ _ -> do mboxUpdate imapc mboxUp
return value
return (resp, value)
NO _ msg -> fail ("NO: " ++ msg)
BAD _ msg -> fail ("BAD: " ++ msg)
PREAUTH _ msg -> fail ("preauth: " ++ msg)
Expand Down Expand Up @@ -266,7 +274,7 @@
do (c, num) <- sendCommand' conn $ "AUTHENTICATE " ++ show at
let challenge =
if BS.take 2 c == BS.pack "+ "
then A.b64Decode $ BS.unpack $ head $

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.8.4

In the use of ‘head’

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / stack / ghc 9.10.2

In the use of ‘head’

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.8.4

In the use of ‘head’

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.10.2

In the use of ‘head’

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / macOS-latest / ghc 9.10.2

In the use of ‘head’

Check warning on line 277 in src/Network/HaskellNet/IMAP.hs

View workflow job for this annotation

GitHub Actions / macOS-latest / ghc 9.8.4

In the use of ‘head’
dropWhile (isSpace . BS.last) $ BS.inits $ BS.drop 2 c
else ""
bsPutCrLf (stream conn) $ BS.pack $
Expand Down Expand Up @@ -333,6 +341,14 @@
appendFull :: IMAPConnection -> MailboxName -> ByteString
-> Maybe [Flag] -> Maybe CalendarTime -> IO ()
appendFull conn mbox mailData flags' time =
appendFullUID conn mbox mailData flags' time >> return ()

-- | Like 'appendFull', but returns the UIDPLUS @APPENDUID@ response code
-- (RFC 4315) when the server supports it, identifying the UID assigned to the
-- appended message. Returns 'Nothing' on servers that don't advertise UIDPLUS.
appendFullUID :: IMAPConnection -> MailboxName -> ByteString
-> Maybe [Flag] -> Maybe CalendarTime -> IO (Maybe AppendUID)
appendFullUID conn mbox mailData flags' time =
do (buf, num) <- sendCommand' conn
(concat ["APPEND ", quoteMailboxName mbox
, fstr, tstr, " {" ++ show len ++ "}"])
Expand All @@ -343,13 +359,16 @@
buf2 <- getResponse $ stream conn
let (resp, mboxUp, ()) = eval pNone (show6 num) buf2
case resp of
OK _ _ -> mboxUpdate conn mboxUp
OK stat _ -> do mboxUpdate conn mboxUp
return $ appendUIDFromStatus stat
NO _ msg -> fail ("NO: "++msg)
BAD _ msg -> fail ("BAD: "++msg)
PREAUTH _ msg -> fail ("PREAUTH: "++msg)
where len = BS.length mailData
tstr = maybe "" ((" "++) . datetimeToStringIMAP) time
fstr = maybe "" ((" ("++) . (++")") . unwords . map show) flags'
appendUIDFromStatus (Just (APPENDUID_sc appendUID')) = Just appendUID'
appendUIDFromStatus _ = Nothing

check :: IMAPConnection -> IO ()
check conn = sendCommand conn "CHECK" pNone
Expand Down Expand Up @@ -771,12 +790,57 @@
store conn i q = storeFull conn (show i) q True >> return ()

copyFull :: IMAPConnection -> String -> String -> IO ()
copyFull conn uidStr mbox =
sendCommand conn ("UID COPY " ++ uidStr ++ " " ++ quoteMailboxName mbox) pNone
copyFull conn uidStr mbox = copyUIDFull conn uidStr mbox >> return ()

-- | Like 'copyFull', but returns the UIDPLUS @COPYUID@ response code (RFC 4315)
-- when the server supports it, identifying the UIDs assigned to the copied
-- messages in the destination mailbox. Returns 'Nothing' on servers that don't
-- advertise UIDPLUS.
copyUIDFull :: IMAPConnection -> String -> String -> IO (Maybe CopyUID)
copyUIDFull conn uidStr mbox =
do (resp, ()) <- sendCommandWithResponse conn ("UID COPY " ++ uidStr ++ " " ++ quoteMailboxName mbox) pNone
return $ copyUIDFromResponse resp
where
copyUIDFromResponse (OK (Just (COPYUID_sc copyUID')) _) = Just copyUID'
copyUIDFromResponse _ = Nothing

copy :: IMAPConnection -> UID -> MailboxName -> IO ()
copy conn uid mbox = copyFull conn (show uid) mbox

-- | Copy a single message to a mailbox, returning its UIDPLUS @COPYUID@ code.
copyUID :: IMAPConnection -> UID -> MailboxName -> IO (Maybe CopyUID)
copyUID conn uid mbox = copyUIDFull conn (show uid) mbox

-- | Copy a set of messages to a mailbox, returning the UIDPLUS @COPYUID@ code.
copyUIDs :: IMAPConnection -> [UID] -> MailboxName -> IO (Maybe CopyUID)
copyUIDs _ [] _ = fail "copyUIDs: empty UID set"
copyUIDs conn uids mbox = copyUIDFull conn (showUIDList uids) mbox

-- | Copy a contiguous UID range (inclusive) to a mailbox, returning the
-- UIDPLUS @COPYUID@ code.
copyUIDR :: IMAPConnection -> (UID, UID) -> MailboxName -> IO (Maybe CopyUID)
copyUIDR conn range mbox = copyUIDFull conn (showUIDRange range) mbox

-- | @UID EXPUNGE@ (RFC 4315): permanently remove only the \\Deleted messages
-- within the given UID set, leaving other \\Deleted messages untouched. Returns
-- the message sequence numbers expunged.
uidExpunge :: IMAPConnection -> [UID] -> IO [Integer]
uidExpunge _ [] = fail "uidExpunge: empty UID set"
uidExpunge conn uids = uidExpungeBySet conn $ showUIDList uids

-- | Like 'uidExpunge' but over a contiguous UID range (inclusive).
uidExpungeR :: IMAPConnection -> (UID, UID) -> IO [Integer]
uidExpungeR conn range = uidExpungeBySet conn $ showUIDRange range

uidExpungeBySet :: IMAPConnection -> UIDSet -> IO [Integer]
uidExpungeBySet conn uidSet = sendCommand conn ("UID EXPUNGE " ++ uidSet) pExpunge

showUIDList :: [UID] -> UIDSet
showUIDList = intercalate "," . map show

showUIDRange :: (UID, UID) -> UIDSet
showUIDRange (start, end) = show start ++ ":" ++ show end

move :: IMAPConnection -> UID -> MailboxName -> IO ()
move conn uid mboxname = sendCommand conn ("UID MOVE " ++ show uid ++ " " ++ quoteMailboxName mboxname) pNone

Expand Down
Loading
Loading