This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_capability processes the CAPABILITY command
|
||||
pub fn handle_capability(mut conn net.TcpConn, tag string) ! {
|
||||
conn.write('* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN\r\n'.bytes())!
|
||||
conn.write('${tag} OK Completed\r\n'.bytes())!
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
import strconv
|
||||
|
||||
// handle_fetch processes the FETCH command
|
||||
pub fn (mut self Session) handle_fetch( tag string, parts []string) ! {
|
||||
mut mailbox:=self.mailbox()!
|
||||
// For simplicity, we support commands like: A001 FETCH 1:* BODY[TEXT]
|
||||
if parts.len < 4 {
|
||||
conn.write('${tag} BAD FETCH requires a message sequence and data item\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
sequence := parts[2]
|
||||
selected_mailbox := server.mailboxes[mailbox_name]
|
||||
// If the sequence is 1:*, iterate over all messages.
|
||||
if sequence == '1:*' {
|
||||
for i, msg in selected_mailbox.messages {
|
||||
flags_str := if msg.flags.len > 0 {
|
||||
'(' + msg.flags.join(' ') + ')'
|
||||
} else {
|
||||
'()'
|
||||
}
|
||||
// In a full implementation, more attributes would be returned.
|
||||
conn.write('* ${i+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
|
||||
}
|
||||
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
|
||||
return true
|
||||
} else {
|
||||
// Otherwise, parse a single message number
|
||||
index := strconv.atoi(parts[2]) or {
|
||||
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
|
||||
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
|
||||
} else {
|
||||
msg := selected_mailbox.messages[index]
|
||||
flags_str := if msg.flags.len > 0 {
|
||||
'(' + msg.flags.join(' ') + ')'
|
||||
} else {
|
||||
'()'
|
||||
}
|
||||
conn.write('* ${index+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
|
||||
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_login processes the LOGIN command
|
||||
pub fn (mut self Session) handle_login(mut conn net.TcpConn, tag string, parts []string, server &IMAPServer) ! {
|
||||
if parts.len < 4 {
|
||||
conn.write('${tag} BAD LOGIN requires username and password\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
// For demo purposes, accept any username/password
|
||||
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] User logged in\r\n'.bytes())!
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_logout processes the LOGOUT command
|
||||
pub fn (mut self Session) handle_logout(mut conn net.TcpConn, tag string) ! {
|
||||
conn.write('* BYE IMAP4rev1 Server logging out\r\n'.bytes())!
|
||||
conn.write('${tag} OK LOGOUT completed\r\n'.bytes())!
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_select processes the SELECT command
|
||||
pub fn (mut self Session) handle_select(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer) !string {
|
||||
if parts.len < 3 {
|
||||
conn.write('${tag} BAD SELECT requires a mailbox name\r\n'.bytes())!
|
||||
return error('SELECT requires a mailbox name')
|
||||
}
|
||||
// Remove any surrounding quotes from mailbox name
|
||||
mailbox_name := parts[2].trim('"')
|
||||
// Look for the mailbox. If not found, create it.
|
||||
if mailbox_name !in server.mailboxes {
|
||||
server.mailboxes[mailbox_name] = &Mailbox{
|
||||
name: mailbox_name
|
||||
messages: []Message{}
|
||||
}
|
||||
}
|
||||
// Respond with a basic status.
|
||||
messages_count := server.mailboxes[mailbox_name].messages.len
|
||||
conn.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n'.bytes())!
|
||||
conn.write('* ${messages_count} EXISTS\r\n'.bytes())!
|
||||
conn.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n'.bytes())!
|
||||
conn.write('${tag} OK [READ-WRITE] SELECT completed\r\n'.bytes())!
|
||||
return mailbox_name
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
import strconv
|
||||
|
||||
// handle_store processes the STORE command
|
||||
pub fn (mut self Session) handle_store(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer, mailbox_name string) !bool {
|
||||
if mailbox_name !in server.mailboxes {
|
||||
conn.write('${tag} BAD No mailbox selected\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
// Expecting a format like: A003 STORE 1 +FLAGS (\Seen)
|
||||
if parts.len < 5 {
|
||||
conn.write('${tag} BAD STORE requires a message sequence, an operation, and flags\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
// For simplicity, only support a single message number.
|
||||
index := strconv.atoi(parts[2]) or {
|
||||
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
|
||||
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
op := parts[3] // e.g. "+FLAGS", "-FLAGS", or "FLAGS"
|
||||
// The flags are provided in the next token, e.g.: (\Seen)
|
||||
flags_str := parts[4]
|
||||
// Remove any surrounding parentheses.
|
||||
flags_clean := flags_str.trim('()')
|
||||
flags_arr := flags_clean.split(' ').filter(it != '')
|
||||
mut msg := server.mailboxes[mailbox_name].messages[index]
|
||||
match op {
|
||||
'+FLAGS' {
|
||||
// Add each flag if it isn't already present.
|
||||
for flag in flags_arr {
|
||||
if flag !in msg.flags {
|
||||
msg.flags << flag
|
||||
}
|
||||
}
|
||||
}
|
||||
'-FLAGS' {
|
||||
// Remove any flags that match.
|
||||
for flag in flags_arr {
|
||||
msg.flags = msg.flags.filter(it != flag)
|
||||
}
|
||||
}
|
||||
'FLAGS' {
|
||||
// Replace current flags.
|
||||
msg.flags = flags_arr
|
||||
}
|
||||
else {
|
||||
conn.write('${tag} BAD Unknown STORE operation\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
}
|
||||
// Save the updated message back.
|
||||
server.mailboxes[mailbox_name].messages[index] = msg
|
||||
conn.write('${tag} OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)] Store completed\r\n'.bytes())!
|
||||
return true
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
module imap
|
||||
import net
|
||||
import io
|
||||
|
||||
// Represents an email message.
|
||||
pub struct Message {
|
||||
pub mut:
|
||||
id int
|
||||
subject string
|
||||
body string
|
||||
flags []string // e.g.: ["\\Seen", "\\Flagged"]
|
||||
}
|
||||
|
||||
// Represents a mailbox holding messages.
|
||||
pub struct Mailbox {
|
||||
pub mut:
|
||||
name string
|
||||
messages []Message
|
||||
}
|
||||
|
||||
// Our in-memory server holds a map of mailbox names to pointers to Mailbox.
|
||||
pub struct IMAPServer {
|
||||
pub mut:
|
||||
mailboxes map[string]&Mailbox
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
server &IMAPServer
|
||||
mailbox string //the name of the mailbox
|
||||
conn net.TcpConn
|
||||
reader &io.BufferedReader
|
||||
}
|
||||
|
||||
|
||||
pub fn (mut self Session) mailbox_new(name string) !&Mailbox{
|
||||
self.mailboxes[name] = &Mailbox{name:name}
|
||||
return self.mailboxes[name]
|
||||
}
|
||||
|
||||
pub fn (mut self Session) mailbox() !&Mailbox{
|
||||
if !(mailbox_name in server.mailboxes) {
|
||||
return error ("mailbox ${self.mailbox} does not exist")
|
||||
}
|
||||
return self.mailboxes[self.mailbox] or { panic(err) }
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
The Internet Message Access Protocol Version 4 Revision 1 (IMAP4rev1) enables clients to access and manage email messages on a server, treating remote mailboxes as if they were local folders. This protocol supports operations such as creating, deleting, and renaming mailboxes; checking for new messages; permanently removing messages; setting and clearing flags; parsing messages; searching; and selectively fetching message attributes and content. IMAP4rev1 is designed for single-server access and does not include functionality for sending emails, which is typically handled by protocols like SMTP.
|
||||
|
||||
## Introduction
|
||||
|
||||
IMAP4rev1 allows clients to interact with email messages on a server, providing functionalities similar to local email management. It enables seamless synchronization between clients and servers, facilitating both online and offline email access.
|
||||
|
||||
## Protocol Overview
|
||||
|
||||
IMAP4rev1 operates over a reliable data stream, typically TCP. Communication involves clients sending commands to the server and receiving responses. Servers can also send untagged responses to inform clients of real-time updates.
|
||||
|
||||
## Commands and Responses
|
||||
|
||||
- **Commands**: Issued by the client to perform actions like selecting a mailbox or fetching messages.
|
||||
- **Responses**: Returned by the server to indicate the status of a command or to provide requested data.
|
||||
|
||||
Commands are categorized as follows:
|
||||
|
||||
- **Authentication Commands**: Manage client authentication.
|
||||
- **Mailbox Commands**: Handle mailbox selection and status.
|
||||
- **Message Commands**: Operate on messages within a mailbox.
|
||||
|
||||
## Mailbox Operations
|
||||
|
||||
Clients can perform various operations on mailboxes, including:
|
||||
|
||||
- **Create**: Establish a new mailbox.
|
||||
- **Delete**: Remove an existing mailbox.
|
||||
- **Rename**: Change the name of a mailbox.
|
||||
- **Subscribe/Unsubscribe**: Manage mailbox subscriptions.
|
||||
- **List**: Retrieve a list of mailboxes.
|
||||
- **Status**: Obtain the status of a mailbox, such as message count.
|
||||
|
||||
## Message Operations
|
||||
|
||||
Within mailboxes, clients can:
|
||||
|
||||
- **Fetch**: Retrieve specific message data.
|
||||
- **Search**: Find messages matching certain criteria.
|
||||
- **Store**: Alter message attributes, like flags.
|
||||
- **Copy**: Duplicate messages to another mailbox.
|
||||
- **Expunge**: Permanently remove messages marked for deletion.
|
||||
|
||||
## Client and Server Responsibilities
|
||||
|
||||
- **Client**: Initiates commands, processes server responses, and maintains synchronization with the server.
|
||||
- **Server**: Executes client commands, sends appropriate responses, and manages the state of mailboxes and messages.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
IMAP4rev1 does not inherently provide encryption. It's essential to implement security measures such as TLS to protect data transmitted between clients and servers.
|
||||
|
||||
## References
|
||||
|
||||
- **IMAP4rev1 Specification**: [RFC 3501](https://datatracker.ietf.org/doc/html/rfc3501)
|
||||
- **Email Message Format**: [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822)
|
||||
- **MIME Standards**: [RFC 2045](https://datatracker.ietf.org/doc/html/rfc2045)
|
||||
- **Multiple Mailbox Support**: [RFC 2244](https://datatracker.ietf.org/doc/html/rfc2244)
|
||||
- **Mail Transfer Protocol**: [RFC 2821](https://datatracker.ietf.org/doc/html/rfc2821)
|
||||
|
||||
For a comprehensive understanding and detailed technical specifications, refer to the full [RFC 3501 document](https://datatracker.ietf.org/doc/html/rfc3501).
|
||||
@@ -4,7 +4,7 @@ A simple IMAP server implementation in V that supports basic mailbox operations.
|
||||
|
||||
## Features
|
||||
|
||||
- In-memory IMAP server implementation
|
||||
- IMAP server implementation with persistent storage via mailbox module
|
||||
- Support for multiple mailboxes
|
||||
- Basic IMAP commands: LOGIN, SELECT, FETCH, STORE, LOGOUT
|
||||
- Message flags support (e.g. \Seen, \Flagged)
|
||||
@@ -48,9 +48,9 @@ curl "imap://localhost/INBOX" -u "user:pass" --ssl-reqd
|
||||
The server consists of three main components:
|
||||
|
||||
1. **Model** (`model.v`): Defines the core data structures
|
||||
- `Message`: Represents an email message with ID, subject, body and flags
|
||||
- `Mailbox`: Contains a collection of messages
|
||||
- `IMAPServer`: Holds the mailboxes map
|
||||
- Uses the `mailbox` module for message storage and retrieval
|
||||
- Handles mailbox operations through a standardized interface
|
||||
- Provides message and mailbox management functionality
|
||||
|
||||
2. **Server** (`server.v`): Handles the IMAP protocol implementation
|
||||
- TCP connection handling
|
||||
@@ -60,6 +60,7 @@ The server consists of three main components:
|
||||
3. **Factory** (`factory.v`): Provides easy server initialization
|
||||
- `start()` function to create and run the server
|
||||
- Initializes example INBOX with sample messages
|
||||
- Sets up mailbox storage backend
|
||||
|
||||
## Supported Commands
|
||||
|
||||
@@ -107,7 +108,6 @@ S: A007 OK LOGOUT completed
|
||||
- The server runs on port 143, which typically requires root privileges. Make sure you have the necessary permissions.
|
||||
- This is a basic implementation for demonstration purposes. For production use, consider adding:
|
||||
- Proper authentication
|
||||
- Persistent storage
|
||||
- Full IMAP command support
|
||||
- TLS encryption
|
||||
- Message parsing and MIME support
|
||||
@@ -7,15 +7,15 @@ import freeflowuniverse.herolib.ui.console
|
||||
// handle_authenticate processes the AUTHENTICATE command
|
||||
pub fn (mut self Session) handle_authenticate(tag string, parts []string) ! {
|
||||
if parts.len < 3 {
|
||||
conn.write('${tag} BAD AUTHENTICATE requires an authentication mechanism\r\n'.bytes())!
|
||||
self.conn.write('${tag} BAD AUTHENTICATE requires an authentication mechanism\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
auth_type := parts[2].to_upper()
|
||||
if auth_type == 'PLAIN' {
|
||||
// Send continuation request for credentials
|
||||
conn.write('+ \r\n'.bytes())!
|
||||
self.conn.write('+ \r\n'.bytes())!
|
||||
// Read base64 credentials
|
||||
creds := reader.read_line() or {
|
||||
creds := self.reader.read_line() or {
|
||||
match err.msg() {
|
||||
'closed' {
|
||||
console.print_debug('Client disconnected during authentication')
|
||||
@@ -33,11 +33,13 @@ pub fn (mut self Session) handle_authenticate(tag string, parts []string) ! {
|
||||
}
|
||||
if creds.len > 0 {
|
||||
// For demo purposes, accept any credentials
|
||||
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] Authentication successful\r\n'.bytes())!
|
||||
// After successful auth, remove STARTTLS and LOGINDISABLED capabilities
|
||||
self.capabilities = ['IMAP4rev2', 'AUTH=PLAIN']
|
||||
self.conn.write('${tag} OK [CAPABILITY IMAP4rev2 AUTH=PLAIN] Authentication successful\r\n'.bytes())!
|
||||
} else {
|
||||
conn.write('${tag} NO Authentication failed\r\n'.bytes())!
|
||||
self.conn.write('${tag} NO Authentication failed\r\n'.bytes())!
|
||||
}
|
||||
} else {
|
||||
conn.write('${tag} NO [ALERT] Unsupported authentication mechanism\r\n'.bytes())!
|
||||
self.conn.write('${tag} NO [ALERT] Unsupported authentication mechanism\r\n'.bytes())!
|
||||
}
|
||||
}
|
||||
28
lib/servers/mail/imap/cmd_capability.v
Normal file
28
lib/servers/mail/imap/cmd_capability.v
Normal file
@@ -0,0 +1,28 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_capability processes the CAPABILITY command
|
||||
// See RFC 3501 Section 6.1.1
|
||||
pub fn (mut self Session) handle_capability(tag string) ! {
|
||||
mut capabilities := []string{}
|
||||
|
||||
// IMAP4rev2 is required and must be included
|
||||
capabilities << 'IMAP4rev2'
|
||||
|
||||
// Required capabilities on cleartext ports
|
||||
if !self.tls_active {
|
||||
capabilities << 'STARTTLS'
|
||||
capabilities << 'LOGINDISABLED'
|
||||
}
|
||||
|
||||
// Required AUTH capability
|
||||
capabilities << 'AUTH=PLAIN'
|
||||
|
||||
// Send capabilities in untagged response
|
||||
// Note: IMAP4rev2 doesn't need to be first, but must be included
|
||||
self.conn.write('* CAPABILITY ${capabilities.join(' ')}\r\n'.bytes())!
|
||||
|
||||
// Send tagged OK response
|
||||
self.conn.write('${tag} OK CAPABILITY completed\r\n'.bytes())!
|
||||
}
|
||||
29
lib/servers/mail/imap/cmd_close.v
Normal file
29
lib/servers/mail/imap/cmd_close.v
Normal file
@@ -0,0 +1,29 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_close processes the CLOSE command
|
||||
// See RFC 3501 Section 6.4.1
|
||||
pub fn (mut self Session) handle_close(tag string) ! {
|
||||
// If no mailbox is selected, return error
|
||||
if self.mailbox == '' {
|
||||
self.conn.write('${tag} NO No mailbox selected\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
mut mbox := self.mailbox()!
|
||||
|
||||
// Remove all messages with \Deleted flag
|
||||
mut new_messages := []Message{}
|
||||
for msg in mbox.messages {
|
||||
if '\\Deleted' !in msg.flags {
|
||||
new_messages << msg
|
||||
}
|
||||
}
|
||||
mbox.messages = new_messages
|
||||
|
||||
// Clear selected mailbox
|
||||
self.mailbox = ''
|
||||
|
||||
self.conn.write('${tag} OK CLOSE completed\r\n'.bytes())!
|
||||
}
|
||||
153
lib/servers/mail/imap/cmd_fetch.v
Normal file
153
lib/servers/mail/imap/cmd_fetch.v
Normal file
@@ -0,0 +1,153 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
import strconv
|
||||
|
||||
// handle_fetch processes the FETCH command
|
||||
// See RFC 3501 Section 6.4.5
|
||||
pub fn (mut self Session) handle_fetch(tag string, parts []string) ! {
|
||||
mut mailbox := self.mailbox()!
|
||||
|
||||
if parts.len < 4 {
|
||||
self.conn.write('${tag} BAD FETCH requires a message sequence and data item\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
sequence := parts[2]
|
||||
// Join remaining parts to handle parenthesized items
|
||||
data_items := parts[3..].join(' ').trim('()')
|
||||
|
||||
// Parse data items, handling quoted strings and parentheses
|
||||
mut items_to_fetch := []string{}
|
||||
mut current_item := ''
|
||||
mut in_brackets := false
|
||||
|
||||
for c in data_items {
|
||||
match c {
|
||||
`[` {
|
||||
in_brackets = true
|
||||
current_item += c.ascii_str()
|
||||
}
|
||||
`]` {
|
||||
in_brackets = false
|
||||
current_item += c.ascii_str()
|
||||
if current_item != '' {
|
||||
items_to_fetch << current_item.trim_space()
|
||||
current_item = ''
|
||||
}
|
||||
}
|
||||
` ` {
|
||||
if in_brackets {
|
||||
current_item += c.ascii_str()
|
||||
} else if current_item != '' {
|
||||
items_to_fetch << current_item.trim_space()
|
||||
current_item = ''
|
||||
}
|
||||
}
|
||||
else {
|
||||
current_item += c.ascii_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
if current_item != '' {
|
||||
items_to_fetch << current_item.trim_space()
|
||||
}
|
||||
|
||||
// Convert to uppercase for matching
|
||||
items_to_fetch = items_to_fetch.map(it.to_upper())
|
||||
|
||||
// Parse sequence range
|
||||
mut start_idx := 0
|
||||
mut end_idx := 0
|
||||
|
||||
if sequence == '1:*' {
|
||||
start_idx = 0
|
||||
end_idx = mailbox.messages.len - 1
|
||||
} else if sequence.contains(':') {
|
||||
range_parts := sequence.split(':')
|
||||
if range_parts.len != 2 {
|
||||
self.conn.write('${tag} BAD Invalid sequence range\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
start_idx = strconv.atoi(range_parts[0]) or {
|
||||
self.conn.write('${tag} BAD Invalid sequence range start\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
if range_parts[1] == '*' {
|
||||
end_idx = mailbox.messages.len - 1
|
||||
} else {
|
||||
end_idx = strconv.atoi(range_parts[1]) or {
|
||||
self.conn.write('${tag} BAD Invalid sequence range end\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
}
|
||||
} else {
|
||||
// Single message number
|
||||
start_idx = strconv.atoi(sequence) or {
|
||||
self.conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
end_idx = start_idx
|
||||
}
|
||||
|
||||
if start_idx < 0 || end_idx >= mailbox.messages.len || start_idx > end_idx {
|
||||
self.conn.write('${tag} NO Invalid message range\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Process messages in range
|
||||
for i := start_idx; i <= end_idx; i++ {
|
||||
msg := mailbox.messages[i]
|
||||
mut response := []string{}
|
||||
|
||||
// Always include UID in FETCH responses
|
||||
response << 'UID ${msg.uid}'
|
||||
|
||||
for item in items_to_fetch {
|
||||
match item {
|
||||
'FLAGS' {
|
||||
flags_str := if msg.flags.len > 0 {
|
||||
msg.flags.join(' ')
|
||||
} else {
|
||||
''
|
||||
}
|
||||
response << 'FLAGS (${flags_str})'
|
||||
}
|
||||
'INTERNALDATE' {
|
||||
response << 'INTERNALDATE "${msg.internal_date.str()}"'
|
||||
}
|
||||
'RFC822.SIZE' {
|
||||
response << 'RFC822.SIZE ${msg.body.len}'
|
||||
}
|
||||
'BODY[TEXT]' {
|
||||
response << 'BODY[TEXT] {${msg.body.len}}\r\n${msg.body}'
|
||||
}
|
||||
'BODY[]', 'BODY.PEEK[]' {
|
||||
// For BODY[], return the full message including headers
|
||||
mut full_msg := 'From: <>\r\n'
|
||||
full_msg += 'Subject: ${msg.subject}\r\n'
|
||||
full_msg += 'Date: ${msg.internal_date.str()}\r\n'
|
||||
full_msg += '\r\n' // Empty line between headers and body
|
||||
full_msg += msg.body
|
||||
response << 'BODY[] {${full_msg.len}}\r\n${full_msg}'
|
||||
}
|
||||
'BODY[HEADER]', 'BODY.PEEK[HEADER]' {
|
||||
// Return just the headers
|
||||
mut headers := 'From: <>\r\n'
|
||||
headers += 'Subject: ${msg.subject}\r\n'
|
||||
headers += 'Date: ${msg.internal_date.str()}\r\n'
|
||||
headers += '\r\n' // Empty line after headers
|
||||
response << 'BODY[HEADER] {${headers.len}}\r\n${headers}'
|
||||
}
|
||||
'ENVELOPE' {
|
||||
// Basic envelope with just subject for now
|
||||
response << 'ENVELOPE (NIL "${msg.subject}" NIL NIL NIL NIL NIL NIL NIL NIL)'
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
self.conn.write('* ${i+1} FETCH (${response.join(' ')})\r\n'.bytes())!
|
||||
}
|
||||
self.conn.write('${tag} OK FETCH completed\r\n'.bytes())!
|
||||
}
|
||||
114
lib/servers/mail/imap/cmd_list.v
Normal file
114
lib/servers/mail/imap/cmd_list.v
Normal file
@@ -0,0 +1,114 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_list processes the LIST command
|
||||
// See RFC 3501 Section 6.3.9
|
||||
pub fn (mut self Session) handle_list(tag string, parts []string) ! {
|
||||
if parts.len < 4 {
|
||||
self.conn.write('${tag} BAD LIST requires reference name and mailbox name\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
reference := parts[2].trim('"')
|
||||
pattern := parts[3].trim('"')
|
||||
|
||||
// For now, we only support empty reference and simple patterns
|
||||
if reference != '' && reference != 'INBOX' {
|
||||
// Just return OK with no results for unsupported references
|
||||
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Handle special case of empty mailbox name
|
||||
if pattern == '' {
|
||||
// Return hierarchy delimiter and root name
|
||||
self.conn.write('* LIST (\\Noselect) "/" ""\r\n'.bytes())!
|
||||
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Handle % wildcard (single level)
|
||||
if pattern == '%' {
|
||||
// List top-level mailboxes
|
||||
for name, mbox in self.server.mailboxes {
|
||||
if !name.contains('/') { // Only top level
|
||||
mut attrs := []string{}
|
||||
if mbox.read_only {
|
||||
attrs << '\\ReadOnly'
|
||||
}
|
||||
// Add child status attributes
|
||||
mut has_children := false
|
||||
for other_name, _ in self.server.mailboxes {
|
||||
if other_name.starts_with(name + '/') {
|
||||
has_children = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if has_children {
|
||||
attrs << '\\HasChildren'
|
||||
} else {
|
||||
attrs << '\\HasNoChildren'
|
||||
}
|
||||
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
|
||||
self.conn.write('* LIST ${attr_str} "/" "${name}"\r\n'.bytes())!
|
||||
}
|
||||
}
|
||||
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Handle * wildcard (multiple levels)
|
||||
if pattern == '*' {
|
||||
// List all mailboxes
|
||||
for name, mbox in self.server.mailboxes {
|
||||
mut attrs := []string{}
|
||||
if mbox.read_only {
|
||||
attrs << '\\ReadOnly'
|
||||
}
|
||||
// Add child status attributes
|
||||
mut has_children := false
|
||||
for other_name, _ in self.server.mailboxes {
|
||||
if other_name.starts_with(name + '/') {
|
||||
has_children = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if has_children {
|
||||
attrs << '\\HasChildren'
|
||||
} else {
|
||||
attrs << '\\HasNoChildren'
|
||||
}
|
||||
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
|
||||
self.conn.write('* LIST ${attr_str} "/" "${name}"\r\n'.bytes())!
|
||||
}
|
||||
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Handle exact mailbox name
|
||||
if pattern in self.server.mailboxes {
|
||||
mbox := self.server.mailboxes[pattern]
|
||||
mut attrs := []string{}
|
||||
if mbox.read_only {
|
||||
attrs << '\\ReadOnly'
|
||||
}
|
||||
// Add child status attributes
|
||||
mut has_children := false
|
||||
for other_name, _ in self.server.mailboxes {
|
||||
if other_name.starts_with(pattern + '/') {
|
||||
has_children = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if has_children {
|
||||
attrs << '\\HasChildren'
|
||||
} else {
|
||||
attrs << '\\HasNoChildren'
|
||||
}
|
||||
attr_str := if attrs.len > 0 { '(${attrs.join(' ')})' } else { '()' }
|
||||
self.conn.write('* LIST ${attr_str} "/" "${pattern}"\r\n'.bytes())!
|
||||
}
|
||||
|
||||
self.conn.write('${tag} OK LIST completed\r\n'.bytes())!
|
||||
}
|
||||
33
lib/servers/mail/imap/cmd_login.v
Normal file
33
lib/servers/mail/imap/cmd_login.v
Normal file
@@ -0,0 +1,33 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_login processes the LOGIN command
|
||||
// See RFC 3501 Section 6.2.3
|
||||
pub fn (mut self Session) handle_login(tag string, parts []string) ! {
|
||||
// Check if LOGINDISABLED is advertised
|
||||
if self.capabilities.contains('LOGINDISABLED') {
|
||||
self.conn.write('${tag} NO [PRIVACYREQUIRED] LOGIN disabled\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
if parts.len < 4 {
|
||||
self.conn.write('${tag} BAD LOGIN requires username and password\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
username := parts[2]
|
||||
password := parts[3]
|
||||
|
||||
// TODO: Implement actual authentication
|
||||
// For demo purposes, accept any username/password
|
||||
// In real implementation:
|
||||
// 1. Validate credentials
|
||||
// 2. If invalid, return: NO [AUTHENTICATIONFAILED] Authentication failed
|
||||
// 3. If valid but can't authorize, return: NO [AUTHORIZATIONFAILED] Authorization failed
|
||||
|
||||
// After successful login:
|
||||
// 1. Send capabilities in OK response
|
||||
// 2. Don't include LOGINDISABLED or STARTTLS in capabilities after login
|
||||
self.conn.write('${tag} OK [CAPABILITY IMAP4rev2 AUTH=PLAIN] LOGIN completed\r\n'.bytes())!
|
||||
}
|
||||
9
lib/servers/mail/imap/cmd_logout.v
Normal file
9
lib/servers/mail/imap/cmd_logout.v
Normal file
@@ -0,0 +1,9 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_logout processes the LOGOUT command
|
||||
pub fn (mut self Session) handle_logout(tag string) ! {
|
||||
self.conn.write('* BYE IMAP4rev2 Server logging out\r\n'.bytes())!
|
||||
self.conn.write('${tag} OK LOGOUT completed\r\n'.bytes())!
|
||||
}
|
||||
53
lib/servers/mail/imap/cmd_select.v
Normal file
53
lib/servers/mail/imap/cmd_select.v
Normal file
@@ -0,0 +1,53 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_select processes the SELECT command
|
||||
// See RFC 3501 Section 6.3.2
|
||||
pub fn (mut self Session) handle_select(tag string, parts []string) ! {
|
||||
if parts.len < 3 {
|
||||
self.conn.write('${tag} BAD SELECT requires a mailbox name\r\n'.bytes())!
|
||||
return error('SELECT requires a mailbox name')
|
||||
}
|
||||
|
||||
// If there's a currently selected mailbox, send CLOSED response
|
||||
if self.mailbox != '' {
|
||||
self.conn.write('* OK [CLOSED] Previous mailbox is now closed\r\n'.bytes())!
|
||||
}
|
||||
|
||||
// Remove any surrounding quotes from mailbox name
|
||||
mailbox_name := parts[2].trim('"')
|
||||
|
||||
// Look for the mailbox
|
||||
if mailbox_name !in self.server.mailboxes {
|
||||
self.conn.write('${tag} NO Mailbox does not exist\r\n'.bytes())!
|
||||
return error('Mailbox does not exist')
|
||||
}
|
||||
|
||||
mut mbox := self.server.mailboxes[mailbox_name]
|
||||
messages_count := mbox.messages.len
|
||||
|
||||
// Required untagged responses per spec:
|
||||
// 1. FLAGS - list of flags that can be set on messages
|
||||
self.conn.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n'.bytes())!
|
||||
|
||||
// 2. EXISTS - number of messages
|
||||
self.conn.write('* ${messages_count} EXISTS\r\n'.bytes())!
|
||||
|
||||
// Required OK untagged responses:
|
||||
// 1. PERMANENTFLAGS
|
||||
self.conn.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n'.bytes())!
|
||||
|
||||
// 2. UIDNEXT
|
||||
self.conn.write('* OK [UIDNEXT ${mbox.next_uid}] Predicted next UID\r\n'.bytes())!
|
||||
|
||||
// 3. UIDVALIDITY
|
||||
self.conn.write('* OK [UIDVALIDITY ${mbox.uid_validity}] UIDs valid\r\n'.bytes())!
|
||||
|
||||
// Update session's selected mailbox
|
||||
self.mailbox = mailbox_name
|
||||
|
||||
// Send READ-WRITE or READ-ONLY status in tagged response
|
||||
// TODO: Implement proper access rights checking
|
||||
self.conn.write('${tag} OK [READ-WRITE] SELECT completed\r\n'.bytes())!
|
||||
}
|
||||
88
lib/servers/mail/imap/cmd_store.v
Normal file
88
lib/servers/mail/imap/cmd_store.v
Normal file
@@ -0,0 +1,88 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
import strconv
|
||||
|
||||
// handle_store processes the STORE command
|
||||
// See RFC 3501 Section 6.4.6
|
||||
pub fn (mut self Session) handle_store(tag string, parts []string) ! {
|
||||
mut mailbox := self.mailbox()!
|
||||
|
||||
// Expecting format like: A003 STORE sequence-set operation flags
|
||||
if parts.len < 5 {
|
||||
self.conn.write('${tag} BAD STORE requires a sequence-set, an operation, and flags\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Parse sequence set (currently only supporting single message numbers)
|
||||
sequence := parts[2]
|
||||
index := strconv.atoi(sequence) or {
|
||||
self.conn.write('${tag} BAD Invalid sequence-set\r\n'.bytes())!
|
||||
return
|
||||
} - 1
|
||||
|
||||
if index < 0 || index >= mailbox.messages.len {
|
||||
self.conn.write('${tag} NO No such message\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Parse operation (FLAGS, +FLAGS, -FLAGS, with optional .SILENT)
|
||||
op := parts[3]
|
||||
silent := op.ends_with('.SILENT')
|
||||
base_op := if silent { op[..op.len-7] } else { op }
|
||||
|
||||
if base_op !in ['FLAGS', '+FLAGS', '-FLAGS'] {
|
||||
self.conn.write('${tag} BAD Unknown STORE operation\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
// Parse flags
|
||||
flags_str := parts[4]
|
||||
flags_clean := flags_str.trim('()')
|
||||
flags_arr := flags_clean.split(' ').filter(it != '')
|
||||
|
||||
// Validate flags
|
||||
valid_flags := ['\\Answered', '\\Flagged', '\\Deleted', '\\Seen', '\\Draft']
|
||||
for flag in flags_arr {
|
||||
if !flag.starts_with('\\') || flag !in valid_flags {
|
||||
self.conn.write('${tag} BAD Invalid flag\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mut msg := mailbox.messages[index]
|
||||
old_flags := msg.flags.clone() // Save for comparison
|
||||
|
||||
// Apply flag changes
|
||||
match base_op {
|
||||
'+FLAGS' {
|
||||
// Add each flag if it isn't already present
|
||||
for flag in flags_arr {
|
||||
if flag !in msg.flags {
|
||||
msg.flags << flag
|
||||
}
|
||||
}
|
||||
}
|
||||
'-FLAGS' {
|
||||
// Remove specified flags
|
||||
for flag in flags_arr {
|
||||
msg.flags = msg.flags.filter(it != flag)
|
||||
}
|
||||
}
|
||||
'FLAGS' {
|
||||
// Replace current flags
|
||||
msg.flags = flags_arr
|
||||
}
|
||||
else {}
|
||||
}
|
||||
|
||||
// Save the updated message
|
||||
mailbox.messages[index] = msg
|
||||
|
||||
// Send untagged FETCH response if flags changed and not silent
|
||||
if !silent && msg.flags != old_flags {
|
||||
self.conn.write('* ${index + 1} FETCH (FLAGS (${msg.flags.join(' ')}))\r\n'.bytes())!
|
||||
}
|
||||
|
||||
self.conn.write('${tag} OK STORE completed\r\n'.bytes())!
|
||||
}
|
||||
53
lib/servers/mail/imap/cmd_uid.v
Normal file
53
lib/servers/mail/imap/cmd_uid.v
Normal file
@@ -0,0 +1,53 @@
|
||||
module imap
|
||||
|
||||
import net
|
||||
|
||||
// handle_uid processes the UID command
|
||||
// See RFC 3501 Section 6.4.9
|
||||
pub fn (mut self Session) handle_uid(tag string, parts []string) ! {
|
||||
if parts.len < 3 {
|
||||
self.conn.write('${tag} BAD UID requires a command\r\n'.bytes())!
|
||||
return
|
||||
}
|
||||
|
||||
subcmd := parts[2].to_upper()
|
||||
match subcmd {
|
||||
'FETCH' {
|
||||
// Remove 'UID' from parts and pass to handle_fetch
|
||||
// The handle_fetch implementation already includes UIDs in responses
|
||||
mut fetch_parts := parts.clone()
|
||||
fetch_parts.delete(1) // Remove 'UID'
|
||||
self.handle_fetch(tag, fetch_parts)!
|
||||
}
|
||||
'SEARCH' {
|
||||
// Remove 'UID' from parts and pass to handle_search
|
||||
mut search_parts := parts.clone()
|
||||
search_parts.delete(1) // Remove 'UID'
|
||||
// TODO: Implement handle_search
|
||||
self.conn.write('${tag} NO SEARCH not implemented\r\n'.bytes())!
|
||||
}
|
||||
'STORE' {
|
||||
// Remove 'UID' from parts and pass to handle_store
|
||||
mut store_parts := parts.clone()
|
||||
store_parts.delete(1) // Remove 'UID'
|
||||
self.handle_store(tag, store_parts)!
|
||||
}
|
||||
'COPY' {
|
||||
// Remove 'UID' from parts and pass to handle_copy
|
||||
mut copy_parts := parts.clone()
|
||||
copy_parts.delete(1) // Remove 'UID'
|
||||
// TODO: Implement handle_copy
|
||||
self.conn.write('${tag} NO COPY not implemented\r\n'.bytes())!
|
||||
}
|
||||
'EXPUNGE' {
|
||||
// Remove 'UID' from parts and pass to handle_expunge
|
||||
mut expunge_parts := parts.clone()
|
||||
expunge_parts.delete(1) // Remove 'UID'
|
||||
// TODO: Implement handle_expunge
|
||||
self.conn.write('${tag} NO EXPUNGE not implemented\r\n'.bytes())!
|
||||
}
|
||||
else {
|
||||
self.conn.write('${tag} BAD Unknown UID command\r\n'.bytes())!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,38 @@
|
||||
|
||||
module imap
|
||||
|
||||
import time
|
||||
pub fn start()! {
|
||||
// Create the server and initialize an example INBOX.
|
||||
mut server := IMAPServer{
|
||||
mailboxes: map[string]&Mailbox{}
|
||||
}
|
||||
|
||||
// Initialize INBOX with required IMAP4rev2 fields
|
||||
mut inbox := Mailbox{
|
||||
name: 'INBOX'
|
||||
next_uid: 3 // Since we have 2 messages
|
||||
uid_validity: u32(time.now().unix()) // Use current time as validity
|
||||
read_only: false
|
||||
messages: [
|
||||
Message{
|
||||
id: 1
|
||||
uid: 1
|
||||
subject: 'Welcome'
|
||||
body: 'Welcome to the IMAP server!'
|
||||
flags: ['\\Seen']
|
||||
internal_date: time.now()
|
||||
},
|
||||
Message{
|
||||
id: 2
|
||||
uid: 2
|
||||
subject: 'Update'
|
||||
body: 'This is an update.'
|
||||
flags: []
|
||||
internal_date: time.now()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Store a pointer to the INBOX.
|
||||
server.mailboxes['INBOX'] = &inbox
|
||||
|
||||
35
lib/servers/mail/imap/model.v
Normal file
35
lib/servers/mail/imap/model.v
Normal file
@@ -0,0 +1,35 @@
|
||||
module imap
|
||||
import net
|
||||
import io
|
||||
import time
|
||||
|
||||
|
||||
// Our in-memory server holds a map of mailbox names to pointers to Mailbox.
|
||||
@[heap]
|
||||
pub struct IMAPServer {
|
||||
pub mut:
|
||||
mailboxes map[string]&Mailbox
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
server &IMAPServer
|
||||
mailbox string // The name of the mailbox
|
||||
conn net.TcpConn
|
||||
reader &io.BufferedReader
|
||||
tls_active bool // Whether TLS is active on the connection
|
||||
capabilities []string // Current capabilities for this session
|
||||
}
|
||||
|
||||
|
||||
pub fn (mut self Session) mailbox_new(name string) !&Mailbox {
|
||||
self.server.mailboxes[name] = &Mailbox{name:name}
|
||||
return self.server.mailboxes[name]
|
||||
}
|
||||
|
||||
pub fn (mut self Session) mailbox() !&Mailbox {
|
||||
if !(self.mailbox in self.server.mailboxes) {
|
||||
return error("mailbox ${self.mailbox} does not exist")
|
||||
}
|
||||
return self.server.mailboxes[self.mailbox] or { panic("bug") }
|
||||
}
|
||||
@@ -40,7 +40,7 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
|
||||
defer {
|
||||
conn.close() or { panic(err) }
|
||||
}
|
||||
conn.write('* OK [CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN] IMAP server ready\r\n'.bytes())!
|
||||
conn.write('* OK [CAPABILITY IMAP4rev2 STARTTLS LOGINDISABLED AUTH=PLAIN] IMAP server ready\r\n'.bytes())!
|
||||
// Initially no mailbox is selected.
|
||||
mut selected_mailbox_name := ''
|
||||
mut res := false
|
||||
@@ -58,6 +58,8 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
|
||||
mailbox: ""
|
||||
conn: conn
|
||||
reader: reader
|
||||
tls_active: false
|
||||
capabilities: ['IMAP4rev2', 'STARTTLS', 'LOGINDISABLED', 'AUTH=PLAIN']
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -99,19 +101,28 @@ fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
|
||||
session.handle_authenticate(tag, parts)!
|
||||
}
|
||||
'SELECT' {
|
||||
session.selected_mailbox_name = handle_select(mut conn, tag, parts, mut server) !
|
||||
session.handle_select(tag, parts)!
|
||||
}
|
||||
'FETCH' {
|
||||
session.handle_fetch(mut conn, tag, parts) !
|
||||
session.handle_fetch(tag, parts) !
|
||||
}
|
||||
'STORE' {
|
||||
session.handle_store(mut conn, tag, parts) !
|
||||
session.handle_store(tag, parts) !
|
||||
}
|
||||
'CAPABILITY' {
|
||||
session.handle_capability(mut conn, tag) !
|
||||
session.handle_capability(tag) !
|
||||
}
|
||||
'LIST' {
|
||||
session.handle_list(tag, parts) !
|
||||
}
|
||||
'UID' {
|
||||
session.handle_uid(tag, parts) !
|
||||
}
|
||||
'CLOSE' {
|
||||
session.handle_close(tag) !
|
||||
}
|
||||
'LOGOUT' {
|
||||
handle_logout(mut conn, tag) !
|
||||
session.handle_logout(tag) !
|
||||
return
|
||||
}
|
||||
else {
|
||||
6875
lib/servers/mail/imap/specs/imap_3501.md
Normal file
6875
lib/servers/mail/imap/specs/imap_3501.md
Normal file
File diff suppressed because it is too large
Load Diff
88
lib/servers/mail/mailbox/README.md
Normal file
88
lib/servers/mail/mailbox/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Mailbox Module
|
||||
|
||||
A V language implementation of a mailbox system that provides core functionality for managing email messages. This module is designed to be used as part of an email server implementation, providing the fundamental storage and retrieval operations for email messages.
|
||||
|
||||
## Features
|
||||
|
||||
- Message management with unique identifiers (UIDs)
|
||||
- CRUD operations for messages (Create, Read, Update, Delete)
|
||||
- Message searching capabilities
|
||||
- Support for message flags (e.g., \Seen, \Flagged)
|
||||
- Read-only mailbox support
|
||||
|
||||
## Core Components
|
||||
|
||||
### Message
|
||||
|
||||
```v
|
||||
pub struct Message {
|
||||
pub mut:
|
||||
uid u32 // Unique identifier for the message
|
||||
subject string
|
||||
body string
|
||||
flags []string // e.g.: ["\Seen", "\Flagged"]
|
||||
internal_date time.Time // Message arrival time
|
||||
}
|
||||
```
|
||||
|
||||
### Mailbox
|
||||
|
||||
```v
|
||||
pub struct Mailbox {
|
||||
pub mut:
|
||||
name string
|
||||
messages []Message
|
||||
next_uid u32 // Next unique identifier to be assigned
|
||||
uid_validity u32 // Unique identifier validity value
|
||||
read_only bool // Whether mailbox is read-only
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Operations
|
||||
|
||||
```v
|
||||
// Create a new mailbox
|
||||
mut mb := Mailbox{
|
||||
name: 'INBOX'
|
||||
next_uid: 1
|
||||
uid_validity: 1
|
||||
}
|
||||
|
||||
// Add a message
|
||||
msg := Message{
|
||||
uid: 1
|
||||
subject: 'Hello'
|
||||
body: 'World'
|
||||
flags: ['\Seen']
|
||||
}
|
||||
mb.set(msg.uid, msg)!
|
||||
|
||||
// Get a message
|
||||
found_msg := mb.get(1)!
|
||||
|
||||
// List all messages
|
||||
messages := mb.list()!
|
||||
|
||||
// Delete a message
|
||||
mb.delete(1)!
|
||||
```
|
||||
|
||||
### Searching Messages
|
||||
|
||||
```v
|
||||
// Search for messages with specific criteria
|
||||
results := mb.find(FindArgs{
|
||||
subject: 'Hello'
|
||||
content: 'World'
|
||||
flags: ['\Seen']
|
||||
})!
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Each message has a unique identifier (UID) that remains constant
|
||||
- The `uid_validity` value helps clients detect mailbox changes
|
||||
- Messages can be flagged with standard IMAP flags
|
||||
- Search operations support filtering by subject, content, and flags
|
||||
47
lib/servers/mail/mailbox/demodata.v
Normal file
47
lib/servers/mail/mailbox/demodata.v
Normal file
@@ -0,0 +1,47 @@
|
||||
module mailbox
|
||||
import time
|
||||
// Creates demo data with 5 user accounts, each having 2 mailboxes and 20 messages
|
||||
pub fn (mut self MailServer) demodata() ! {
|
||||
usernames := ['user1', 'user2', 'user3', 'user4', 'user5']
|
||||
names := ['First User', 'Second User', 'Third User', 'Fourth User', 'Fifth User']
|
||||
|
||||
for i, username in usernames {
|
||||
// Create primary and alternate email addresses
|
||||
primary_email := '${username}@example.com'
|
||||
alt_email := '${username}.alt@example.com'
|
||||
emails := [primary_email, alt_email]
|
||||
|
||||
// Create user account
|
||||
mut account := self.create_account(username, names[i], emails) or { return err }
|
||||
|
||||
// Create second mailbox (INBOX is created by default)
|
||||
account.create_mailbox('Sent') or { return err }
|
||||
|
||||
// Get both mailboxes
|
||||
mut inbox := account.get_mailbox('INBOX') or { return err }
|
||||
mut sent := account.get_mailbox('Sent') or { return err }
|
||||
|
||||
// Add 10 messages to each mailbox
|
||||
for j in 0..10 {
|
||||
// Add message to INBOX
|
||||
inbox_msg := Message{
|
||||
uid: inbox.next_uid + u32(j)
|
||||
subject: 'Inbox Message ${j + 1}'
|
||||
body: 'This is inbox message ${j + 1} for ${username}'
|
||||
flags: if j % 2 == 0 { ['\\Seen'] } else { [] }
|
||||
internal_date: time.now()
|
||||
}
|
||||
inbox.set(inbox_msg.uid, inbox_msg) or { return err }
|
||||
|
||||
// Add message to Sent
|
||||
sent_msg := Message{
|
||||
uid: sent.next_uid + u32(j)
|
||||
subject: 'Sent Message ${j + 1}'
|
||||
body: 'This is sent message ${j + 1} from ${username}'
|
||||
flags: ['\\Seen']
|
||||
internal_date: time.now()
|
||||
}
|
||||
sent.set(sent_msg.uid, sent_msg) or { return err }
|
||||
}
|
||||
}
|
||||
}
|
||||
64
lib/servers/mail/mailbox/demodata_test.v
Normal file
64
lib/servers/mail/mailbox/demodata_test.v
Normal file
@@ -0,0 +1,64 @@
|
||||
module mailbox
|
||||
|
||||
fn new_mailserver() &MailServer {
|
||||
return &MailServer{
|
||||
accounts: map[string]&UserAccount{}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_demodata() ! {
|
||||
mut server := new_mailserver()
|
||||
server.demodata()!
|
||||
|
||||
// Test user accounts
|
||||
usernames := ['user1', 'user2', 'user3', 'user4', 'user5']
|
||||
names := ['First User', 'Second User', 'Third User', 'Fourth User', 'Fifth User']
|
||||
|
||||
for i, username in usernames {
|
||||
// Verify user account exists and properties are correct
|
||||
mut account := server.get_account(username)!
|
||||
assert account.name == username
|
||||
assert account.description == names[i]
|
||||
assert account.emails.len == 2
|
||||
assert account.emails[0] == '${username}@example.com'
|
||||
assert account.emails[1] == '${username}.alt@example.com'
|
||||
|
||||
// Verify mailboxes exist
|
||||
mailboxes := account.list_mailboxes()
|
||||
assert mailboxes.len == 2
|
||||
assert mailboxes.contains('INBOX')
|
||||
assert mailboxes.contains('Sent')
|
||||
|
||||
// Verify INBOX messages
|
||||
mut inbox := account.get_mailbox('INBOX')!
|
||||
messages := inbox.list()!
|
||||
assert messages.len == 10
|
||||
|
||||
// Check specific properties of first and last INBOX messages
|
||||
first_msg := inbox.get(messages[0].uid)!
|
||||
assert first_msg.subject == 'Inbox Message 1'
|
||||
assert first_msg.body == 'This is inbox message 1 for ${username}'
|
||||
assert first_msg.flags == ['\\Seen']
|
||||
|
||||
last_msg := inbox.get(messages[9].uid)!
|
||||
assert last_msg.subject == 'Inbox Message 10'
|
||||
assert last_msg.body == 'This is inbox message 10 for ${username}'
|
||||
assert last_msg.flags == if 9 % 2 == 0 { ['\\Seen'] } else { [] }
|
||||
|
||||
// Verify Sent messages
|
||||
mut sent := account.get_mailbox('Sent')!
|
||||
sent_messages := sent.list()!
|
||||
assert sent_messages.len == 10
|
||||
|
||||
// Check specific properties of first and last Sent messages
|
||||
first_sent := sent.get(sent_messages[0].uid)!
|
||||
assert first_sent.subject == 'Sent Message 1'
|
||||
assert first_sent.body == 'This is sent message 1 from ${username}'
|
||||
assert first_sent.flags == ['\\Seen']
|
||||
|
||||
last_sent := sent.get(sent_messages[9].uid)!
|
||||
assert last_sent.subject == 'Sent Message 10'
|
||||
assert last_sent.body == 'This is sent message 10 from ${username}'
|
||||
assert last_sent.flags == ['\\Seen']
|
||||
}
|
||||
}
|
||||
104
lib/servers/mail/mailbox/mailbox.v
Normal file
104
lib/servers/mail/mailbox/mailbox.v
Normal file
@@ -0,0 +1,104 @@
|
||||
module mailbox
|
||||
|
||||
// Represents a mailbox holding messages.
|
||||
@[heap]
|
||||
pub struct Mailbox {
|
||||
pub mut:
|
||||
name string
|
||||
messages []Message
|
||||
next_uid u32 // Next unique identifier to be assigned
|
||||
uid_validity u32 // Unique identifier validity value
|
||||
read_only bool // Whether mailbox is read-only
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Returns all messages in the mailbox
|
||||
pub fn (mut self Mailbox) list() ![]Message {
|
||||
return self.messages
|
||||
}
|
||||
|
||||
// Gets a message by its UID
|
||||
pub fn (mut self Mailbox) get(uid u32) !Message {
|
||||
for msg in self.messages {
|
||||
if msg.uid == uid {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
return error('Message with UID ${uid} not found')
|
||||
}
|
||||
|
||||
// Deletes a message by its UID
|
||||
pub fn (mut self Mailbox) delete(uid u32) ! {
|
||||
for i, msg in self.messages {
|
||||
if msg.uid == uid {
|
||||
self.messages.delete(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
return error('Message with UID ${uid} not found')
|
||||
}
|
||||
|
||||
// Sets/updates a message with the given UID
|
||||
pub fn (mut self Mailbox) set(uid u32, msg Message) ! {
|
||||
if self.read_only {
|
||||
return error('Mailbox is read-only')
|
||||
}
|
||||
|
||||
mut found := false
|
||||
for i, existing in self.messages {
|
||||
if existing.uid == uid {
|
||||
self.messages[i] = msg
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// Add as new message if UID doesn't exist
|
||||
self.messages << msg
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct FindArgs{
|
||||
pub mut:
|
||||
subject string
|
||||
content string
|
||||
flags []string
|
||||
}
|
||||
|
||||
// Finds messages matching the given criteria
|
||||
pub fn (mut self Mailbox) find(args FindArgs) ![]Message {
|
||||
mut results := []Message{}
|
||||
|
||||
for msg in self.messages {
|
||||
mut matches := true
|
||||
|
||||
// Check subject if specified
|
||||
if args.subject != '' && !msg.subject.contains(args.subject) {
|
||||
matches = false
|
||||
}
|
||||
|
||||
// Check content if specified
|
||||
if matches && args.content != '' && !msg.body.contains(args.content) {
|
||||
matches = false
|
||||
}
|
||||
|
||||
// Check all specified flags are present
|
||||
if matches && args.flags.len > 0 {
|
||||
for flag in args.flags {
|
||||
if flag !in msg.flags {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches {
|
||||
results << msg
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
142
lib/servers/mail/mailbox/mailbox_test.v
Normal file
142
lib/servers/mail/mailbox/mailbox_test.v
Normal file
@@ -0,0 +1,142 @@
|
||||
module mailbox
|
||||
|
||||
import time
|
||||
|
||||
fn test_mailbox_basic_operations() {
|
||||
mut mb := Mailbox{
|
||||
name: 'INBOX'
|
||||
uid_validity: 1234
|
||||
}
|
||||
|
||||
// Test empty mailbox
|
||||
msgs := mb.list() or { panic(err) }
|
||||
assert msgs.len == 0
|
||||
|
||||
// Test adding a message
|
||||
msg1 := Message{
|
||||
uid: 1
|
||||
subject: 'Test email'
|
||||
body: 'Hello world'
|
||||
flags: ['\\Seen']
|
||||
internal_date: time.now()
|
||||
}
|
||||
mb.set(1, msg1) or { panic(err) }
|
||||
|
||||
// Test listing messages
|
||||
msgs2 := mb.list() or { panic(err) }
|
||||
assert msgs2.len == 1
|
||||
assert msgs2[0].subject == 'Test email'
|
||||
|
||||
// Test getting message by UID
|
||||
found := mb.get(1) or { panic(err) }
|
||||
assert found.uid == 1
|
||||
assert found.subject == 'Test email'
|
||||
assert found.body == 'Hello world'
|
||||
assert found.flags == ['\\Seen']
|
||||
}
|
||||
|
||||
fn test_mailbox_delete() {
|
||||
mut mb := Mailbox{
|
||||
name: 'INBOX'
|
||||
uid_validity: 1234
|
||||
}
|
||||
|
||||
// Add two messages
|
||||
msg1 := Message{
|
||||
uid: 1
|
||||
subject: 'First email'
|
||||
body: 'Content 1'
|
||||
}
|
||||
msg2 := Message{
|
||||
uid: 2
|
||||
subject: 'Second email'
|
||||
body: 'Content 2'
|
||||
}
|
||||
mb.set(1, msg1) or { panic(err) }
|
||||
mb.set(2, msg2) or { panic(err) }
|
||||
|
||||
// Delete first message
|
||||
mb.delete(1) or { panic(err) }
|
||||
|
||||
// Verify only second message remains
|
||||
msgs := mb.list() or { panic(err) }
|
||||
assert msgs.len == 1
|
||||
assert msgs[0].uid == 2
|
||||
assert msgs[0].subject == 'Second email'
|
||||
|
||||
// Test deleting non-existent message
|
||||
if _ := mb.delete(999) {
|
||||
panic('Expected error when deleting non-existent message')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_mailbox_find() {
|
||||
mut mb := Mailbox{
|
||||
name: 'INBOX'
|
||||
uid_validity: 1234
|
||||
}
|
||||
|
||||
// Add test messages
|
||||
msg1 := Message{
|
||||
uid: 1
|
||||
subject: 'Important meeting'
|
||||
body: 'Meeting at 2 PM'
|
||||
flags: ['\\Seen', '\\Flagged']
|
||||
}
|
||||
msg2 := Message{
|
||||
uid: 2
|
||||
subject: 'Hello friend'
|
||||
body: 'How are you?'
|
||||
flags: ['\\Seen']
|
||||
}
|
||||
msg3 := Message{
|
||||
uid: 3
|
||||
subject: 'Another meeting'
|
||||
body: 'Team sync at 3 PM'
|
||||
flags: ['\\Draft']
|
||||
}
|
||||
|
||||
mb.set(1, msg1) or { panic(err) }
|
||||
mb.set(2, msg2) or { panic(err) }
|
||||
mb.set(3, msg3) or { panic(err) }
|
||||
|
||||
// Test finding by subject
|
||||
found_subject := mb.find(FindArgs{subject: 'meeting'}) or { panic(err) }
|
||||
assert found_subject.len == 2
|
||||
|
||||
// Test finding by content
|
||||
found_content := mb.find(FindArgs{content: 'PM'}) or { panic(err) }
|
||||
assert found_content.len == 2
|
||||
|
||||
// Test finding by flags
|
||||
found_flags := mb.find(FindArgs{flags: ['\\Seen', '\\Flagged']}) or { panic(err) }
|
||||
assert found_flags.len == 1
|
||||
assert found_flags[0].uid == 1
|
||||
|
||||
// Test finding with multiple criteria
|
||||
found_multi := mb.find(FindArgs{
|
||||
subject: 'meeting'
|
||||
flags: ['\\Draft']
|
||||
}) or { panic(err) }
|
||||
assert found_multi.len == 1
|
||||
assert found_multi[0].uid == 3
|
||||
}
|
||||
|
||||
fn test_readonly_mailbox() {
|
||||
mut mb := Mailbox{
|
||||
name: 'INBOX'
|
||||
uid_validity: 1234
|
||||
read_only: true
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
uid: 1
|
||||
subject: 'Test email'
|
||||
body: 'Hello world'
|
||||
}
|
||||
|
||||
// Attempt to modify read-only mailbox should fail
|
||||
if _ := mb.set(1, msg) {
|
||||
panic('Expected error when modifying read-only mailbox')
|
||||
}
|
||||
}
|
||||
69
lib/servers/mail/mailbox/mailserver.v
Normal file
69
lib/servers/mail/mailbox/mailserver.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module mailbox
|
||||
import time
|
||||
|
||||
// Represents the mail server that manages user accounts
|
||||
@[heap]
|
||||
pub struct MailServer {
|
||||
pub mut:
|
||||
accounts map[string]&UserAccount // Map of username to user account
|
||||
}
|
||||
|
||||
// Creates a new user account
|
||||
pub fn (mut self MailServer) create_account(username string, description string, emails []string) !&UserAccount {
|
||||
if username in self.accounts {
|
||||
return error('User ${username} already exists')
|
||||
}
|
||||
|
||||
// Verify emails are unique across all accounts
|
||||
for _, account in self.accounts {
|
||||
for email in emails {
|
||||
if email in account.emails {
|
||||
return error('Email ${email} is already registered to another account')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mut account := &UserAccount{
|
||||
name: username
|
||||
description: description
|
||||
emails: emails.clone()
|
||||
mailboxes: map[string]&Mailbox{}
|
||||
}
|
||||
self.accounts[username] = account
|
||||
|
||||
// Create default INBOX mailbox
|
||||
account.create_mailbox('INBOX') or { return err }
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
// Gets a user account by username
|
||||
pub fn (mut self MailServer) get_account(username string) !&UserAccount {
|
||||
if account := self.accounts[username] {
|
||||
return account
|
||||
}
|
||||
return error('User ${username} not found')
|
||||
}
|
||||
|
||||
// Deletes a user account
|
||||
pub fn (mut self MailServer) delete_account(username string) ! {
|
||||
if username !in self.accounts {
|
||||
return error('User ${username} not found')
|
||||
}
|
||||
self.accounts.delete(username)
|
||||
}
|
||||
|
||||
// Lists all usernames
|
||||
pub fn (self MailServer) list_accounts() []string {
|
||||
return self.accounts.keys()
|
||||
}
|
||||
|
||||
// Finds account by email address
|
||||
pub fn (mut self MailServer) find_account_by_email(email string) !&UserAccount {
|
||||
for _, account in self.accounts {
|
||||
if email in account.emails {
|
||||
return account
|
||||
}
|
||||
}
|
||||
return error('No account found with email ${email}')
|
||||
}
|
||||
13
lib/servers/mail/mailbox/message.v
Normal file
13
lib/servers/mail/mailbox/message.v
Normal file
@@ -0,0 +1,13 @@
|
||||
module mailbox
|
||||
import time
|
||||
|
||||
// Represents an email message.
|
||||
pub struct Message {
|
||||
pub mut:
|
||||
uid u32 // Unique identifier for the message
|
||||
subject string
|
||||
body string
|
||||
flags []string // e.g.: ["\\Seen", "\\Flagged"]
|
||||
internal_date time.Time // Message arrival time
|
||||
}
|
||||
|
||||
47
lib/servers/mail/mailbox/useraccount.v
Normal file
47
lib/servers/mail/mailbox/useraccount.v
Normal file
@@ -0,0 +1,47 @@
|
||||
module mailbox
|
||||
import time
|
||||
|
||||
// Represents a user account in the mail server
|
||||
@[heap]
|
||||
pub struct UserAccount {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
emails []string
|
||||
mailboxes map[string]&Mailbox // Map of mailbox name to mailbox instance
|
||||
}
|
||||
|
||||
// Creates a new mailbox for the user account
|
||||
pub fn (mut self UserAccount) create_mailbox(name string) !&Mailbox {
|
||||
if name in self.mailboxes {
|
||||
return error('Mailbox ${name} already exists')
|
||||
}
|
||||
|
||||
mb := &Mailbox{
|
||||
name: name
|
||||
uid_validity: u32(time.now().unix())
|
||||
}
|
||||
self.mailboxes[name] = mb
|
||||
return mb
|
||||
}
|
||||
|
||||
// Gets a mailbox by name
|
||||
pub fn (mut self UserAccount) get_mailbox(name string) !&Mailbox {
|
||||
if mailbox := self.mailboxes[name] {
|
||||
return mailbox
|
||||
}
|
||||
return error('Mailbox ${name} not found')
|
||||
}
|
||||
|
||||
// Deletes a mailbox by name
|
||||
pub fn (mut self UserAccount) delete_mailbox(name string) ! {
|
||||
if name !in self.mailboxes {
|
||||
return error('Mailbox ${name} not found')
|
||||
}
|
||||
self.mailboxes.delete(name)
|
||||
}
|
||||
|
||||
// Lists all mailboxes for the user
|
||||
pub fn (self UserAccount) list_mailboxes() []string {
|
||||
return self.mailboxes.keys()
|
||||
}
|
||||
134
lib/servers/mail/mailbox/useraccount_test.v
Normal file
134
lib/servers/mail/mailbox/useraccount_test.v
Normal file
@@ -0,0 +1,134 @@
|
||||
module mailbox
|
||||
import time
|
||||
|
||||
fn test_user_account_mailboxes() {
|
||||
mut account := UserAccount{
|
||||
name: 'testuser'
|
||||
description: 'Test User'
|
||||
emails: ['test@example.com']
|
||||
}
|
||||
|
||||
// Test creating mailboxes
|
||||
inbox := account.create_mailbox('INBOX') or { panic(err) }
|
||||
assert inbox.name == 'INBOX'
|
||||
|
||||
sent := account.create_mailbox('Sent') or { panic(err) }
|
||||
assert sent.name == 'Sent'
|
||||
|
||||
// Test duplicate mailbox creation
|
||||
if _ := account.create_mailbox('INBOX') {
|
||||
panic('Expected error when creating duplicate mailbox')
|
||||
}
|
||||
|
||||
// Test listing mailboxes
|
||||
boxes := account.list_mailboxes()
|
||||
assert boxes.len == 2
|
||||
assert 'INBOX' in boxes
|
||||
assert 'Sent' in boxes
|
||||
|
||||
// Test getting mailbox
|
||||
found := account.get_mailbox('INBOX') or { panic(err) }
|
||||
assert found.name == 'INBOX'
|
||||
|
||||
// Test getting non-existent mailbox
|
||||
if _ := account.get_mailbox('NonExistent') {
|
||||
panic('Expected error when getting non-existent mailbox')
|
||||
}
|
||||
|
||||
// Test deleting mailbox
|
||||
account.delete_mailbox('Sent') or { panic(err) }
|
||||
boxes_after_delete := account.list_mailboxes()
|
||||
assert boxes_after_delete.len == 1
|
||||
assert 'Sent' !in boxes_after_delete
|
||||
}
|
||||
|
||||
fn test_mail_server_accounts() {
|
||||
mut server := MailServer{}
|
||||
|
||||
// Test creating accounts
|
||||
mut account1 := server.create_account('user1', 'First User', ['user1@example.com', 'user1.alt@example.com']) or { panic(err) }
|
||||
assert account1.name == 'user1'
|
||||
assert account1.emails.len == 2
|
||||
|
||||
// Verify INBOX was created automatically
|
||||
mut inbox := account1.get_mailbox('INBOX') or { panic(err) }
|
||||
assert inbox.name == 'INBOX'
|
||||
|
||||
// Test creating account with duplicate username
|
||||
if _ := server.create_account('user1', 'Duplicate User', ['other@example.com']) {
|
||||
panic('Expected error when creating account with duplicate username')
|
||||
}
|
||||
|
||||
// Test creating account with duplicate email
|
||||
if _ := server.create_account('user2', 'Second User', ['user1@example.com']) {
|
||||
panic('Expected error when creating account with duplicate email')
|
||||
}
|
||||
|
||||
// Test creating another valid account
|
||||
mut account2 := server.create_account('user2', 'Second User', ['user2@example.com']) or { panic(err) }
|
||||
assert account2.name == 'user2'
|
||||
|
||||
// Test listing accounts
|
||||
accounts := server.list_accounts()
|
||||
assert accounts.len == 2
|
||||
assert 'user1' in accounts
|
||||
assert 'user2' in accounts
|
||||
|
||||
// Test getting account
|
||||
mut found := server.get_account('user1') or { panic(err) }
|
||||
assert found.name == 'user1'
|
||||
assert found.emails == ['user1@example.com', 'user1.alt@example.com']
|
||||
|
||||
// Test getting non-existent account
|
||||
if _ := server.get_account('nonexistent') {
|
||||
panic('Expected error when getting non-existent account')
|
||||
}
|
||||
|
||||
// Test finding account by email
|
||||
mut found_by_email := server.find_account_by_email('user1.alt@example.com') or { panic(err) }
|
||||
assert found_by_email.name == 'user1'
|
||||
|
||||
// Test finding non-existent email
|
||||
if _ := server.find_account_by_email('nonexistent@example.com') {
|
||||
panic('Expected error when finding non-existent email')
|
||||
}
|
||||
|
||||
// Test deleting account
|
||||
server.delete_account('user2') or { panic(err) }
|
||||
accounts_after_delete := server.list_accounts()
|
||||
assert accounts_after_delete.len == 1
|
||||
assert 'user2' !in accounts_after_delete
|
||||
}
|
||||
|
||||
fn test_end_to_end() {
|
||||
mut server := MailServer{}
|
||||
|
||||
// Create account
|
||||
mut account := server.create_account('testuser', 'Test User', ['test@example.com']) or { panic(err) }
|
||||
|
||||
// Get INBOX and add a message
|
||||
mut inbox := account.get_mailbox('INBOX') or { panic(err) }
|
||||
msg := Message{
|
||||
uid: 1
|
||||
subject: 'Test message'
|
||||
body: 'Hello world'
|
||||
flags: ['\\Seen']
|
||||
}
|
||||
inbox.set(1, msg) or { panic(err) }
|
||||
|
||||
// Create Archives mailbox
|
||||
mut archives := account.create_mailbox('Archives') or { panic(err) }
|
||||
|
||||
// Verify mailboxes through server lookup
|
||||
mut found_account := server.get_account('testuser') or { panic(err) }
|
||||
mailboxes := found_account.list_mailboxes()
|
||||
assert mailboxes.len == 2
|
||||
assert 'INBOX' in mailboxes
|
||||
assert 'Archives' in mailboxes
|
||||
|
||||
// Verify message in INBOX
|
||||
mut found_inbox := found_account.get_mailbox('INBOX') or { panic(err) }
|
||||
msgs := found_inbox.list() or { panic(err) }
|
||||
assert msgs.len == 1
|
||||
assert msgs[0].subject == 'Test message'
|
||||
}
|
||||
Reference in New Issue
Block a user