feat: Add contacts database and VFS implementation

- Added a new contacts database (`ContactsDB`) to store contact
  information.  This improves data organization and allows for
  more efficient querying and manipulation of contact data.
- Implemented a virtual file system (VFS) for contacts
  (`vfs_contacts`). This provides a file-like interface to access
  and manage contact data, improving integration with existing
  file-system-based tools and workflows.  The VFS supports
  listing by group, by name, and by email.
- Added model structs for contacts, improving data organization and
  serialization.  This lays the foundation for more robust data
  handling and future expansion.
This commit is contained in:
Mahmoud Emad
2025-03-17 15:58:20 +02:00
parent c47002430e
commit abd694015b
14 changed files with 1111 additions and 299 deletions

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.db as core
// new creates a new mail VFS instance
pub fn new(mail_db &core.MailDB) !vfs.VFSImplementation {

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.models.mcc.mail
import freeflowuniverse.herolib.circles.mcc.models as mail
// MailFSEntry implements FSEntry for mail objects
pub struct MailFSEntry {

View File

@@ -1,26 +1,24 @@
module vfs_mail
import json
import os
import time
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.models.mcc.mail
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.models as mail
import freeflowuniverse.herolib.core.texttools
// Basic operations
pub fn (mut myvfs MailVFS) root_get() !vfs.FSEntry {
metadata := vfs.Metadata{
id: 1
name: ''
file_type: .directory
created_at: time.now().unix()
id: 1
name: ''
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: ''
path: ''
metadata: metadata
}
}
@@ -34,18 +32,18 @@ pub fn (mut myvfs MailVFS) file_read(path string) ![]u8 {
if !myvfs.exists(path) {
return error('File does not exist: ${path}')
}
entry := myvfs.get(path)!
if !entry.is_file() {
return error('Path is not a file: ${path}')
}
mail_entry := entry as MailFSEntry
if email := mail_entry.email {
return json.encode(email).bytes()
}
return error('Failed to read file: ${path}')
}
@@ -70,15 +68,15 @@ pub fn (mut myvfs MailVFS) dir_list(path string) ![]vfs.FSEntry {
if !myvfs.exists(path) {
return error('Directory does not exist: ${path}')
}
// Get all emails
emails := myvfs.mail_db.getall() or { return error('Failed to get emails: ${err}') }
// If we're at the root, return all mailboxes
if path == '' {
return myvfs.list_mailboxes(emails)!
}
// Check if we're in a mailbox path
path_parts := path.split('/')
if path_parts.len == 1 {
@@ -88,7 +86,7 @@ pub fn (mut myvfs MailVFS) dir_list(path string) ![]vfs.FSEntry {
// We're in an id or subject directory, list the emails
return myvfs.list_emails_by_type(path_parts[0], path_parts[1], emails)!
}
return []vfs.FSEntry{}
}
@@ -115,17 +113,17 @@ pub fn (mut myvfs MailVFS) exists(path string) bool {
if path == '' {
return true
}
// Get all emails
emails := myvfs.mail_db.getall() or { return false }
// Debug print
if path.contains('subject') {
println('Checking exists for path: ${path}')
}
path_parts := path.split('/')
// Check if the path is a mailbox
if path_parts.len == 1 {
for email in emails {
@@ -135,7 +133,7 @@ pub fn (mut myvfs MailVFS) exists(path string) bool {
}
}
}
// Check if the path is a mailbox subdir (id or subject)
if path_parts.len == 2 && path_parts[1] in ['id', 'subject'] {
for email in emails {
@@ -145,14 +143,14 @@ pub fn (mut myvfs MailVFS) exists(path string) bool {
}
}
}
// Check if the path is an email file
if path_parts.len == 3 && path_parts[1] in ['id', 'subject'] {
for email in emails {
if email.mailbox.split('/')[0] != path_parts[0] {
continue
}
if path_parts[1] == 'id' && '${email.id}.json' == path_parts[2] {
return true
} else if path_parts[1] == 'subject' {
@@ -170,7 +168,7 @@ pub fn (mut myvfs MailVFS) exists(path string) bool {
}
}
}
return false
}
@@ -179,13 +177,13 @@ pub fn (mut myvfs MailVFS) get(path string) !vfs.FSEntry {
if path == '' {
return myvfs.root_get()!
}
// Debug print
println('Getting path: ${path}')
// Get all emails
emails := myvfs.mail_db.getall() or { return error('Failed to get emails: ${err}') }
// Debug: Print all emails
println('All emails in DB:')
for email in emails {
@@ -193,96 +191,96 @@ pub fn (mut myvfs MailVFS) get(path string) !vfs.FSEntry {
println('Email ID: ${email.id}, Subject: "${envelope.subject}", Mailbox: ${email.mailbox}')
}
}
path_parts := path.split('/')
// Check if the path is a mailbox
if path_parts.len == 1 {
for email in emails {
mailbox_parts := email.mailbox.split('/')
if mailbox_parts.len > 0 && mailbox_parts[0] == path_parts[0] {
metadata := vfs.Metadata{
id: u32(path_parts[0].bytes().bytestr().hash())
name: path_parts[0]
file_type: .directory
created_at: time.now().unix()
id: u32(path_parts[0].bytes().bytestr().hash())
name: path_parts[0]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
}
}
}
}
// Check if the path is a mailbox subdir (id or subject)
if path_parts.len == 2 && path_parts[1] in ['id', 'subject'] {
metadata := vfs.Metadata{
id: u32(path.bytes().bytestr().hash())
name: path_parts[1]
file_type: .directory
created_at: time.now().unix()
id: u32(path.bytes().bytestr().hash())
name: path_parts[1]
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
}
}
// Check if the path is an email file
if path_parts.len == 3 && path_parts[1] in ['id', 'subject'] {
for email in emails {
if email.mailbox.split('/')[0] != path_parts[0] {
continue
}
if path_parts[1] == 'id' && '${email.id}.json' == path_parts[2] {
metadata := vfs.Metadata{
id: email.id
name: '${email.id}.json'
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: '${email.id}.json'
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
email: email
email: email
}
} else if path_parts[1] == 'subject' {
if envelope := email.envelope {
subject_filename := texttools.name_fix(envelope.subject) + '.json'
if subject_filename == path_parts[2] {
metadata := vfs.Metadata{
id: email.id
name: subject_filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: subject_filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
return MailFSEntry{
path: path
path: path
metadata: metadata
email: email
email: email
}
}
}
}
}
}
return error('Path not found: ${path}')
}
@@ -320,7 +318,7 @@ pub fn (mut myvfs MailVFS) destroy() ! {
// Helper functions
fn (mut myvfs MailVFS) list_mailboxes(emails []mail.Email) ![]vfs.FSEntry {
mut mailboxes := map[string]bool{}
// Collect unique top-level mailbox names
for email in emails {
mailbox_parts := email.mailbox.split('/')
@@ -328,111 +326,110 @@ fn (mut myvfs MailVFS) list_mailboxes(emails []mail.Email) ![]vfs.FSEntry {
mailboxes[mailbox_parts[0]] = true
}
}
// Create FSEntry for each mailbox
mut result := []vfs.FSEntry{cap: mailboxes.len}
for mailbox, _ in mailboxes {
metadata := vfs.Metadata{
id: u32(mailbox.bytes().bytestr().hash())
name: mailbox
file_type: .directory
created_at: time.now().unix()
id: u32(mailbox.bytes().bytestr().hash())
name: mailbox
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: mailbox
path: mailbox
metadata: metadata
}
}
return result
}
fn (mut myvfs MailVFS) list_mailbox_subdirs(mailbox string) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{cap: 2}
// Create id directory
id_metadata := vfs.Metadata{
id: u32('${mailbox}/id'.bytes().bytestr().hash())
name: 'id'
file_type: .directory
created_at: time.now().unix()
id: u32('${mailbox}/id'.bytes().bytestr().hash())
name: 'id'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/id'
path: '${mailbox}/id'
metadata: id_metadata
}
// Create subject directory
subject_metadata := vfs.Metadata{
id: u32('${mailbox}/subject'.bytes().bytestr().hash())
name: 'subject'
file_type: .directory
created_at: time.now().unix()
id: u32('${mailbox}/subject'.bytes().bytestr().hash())
name: 'subject'
file_type: .directory
created_at: time.now().unix()
modified_at: time.now().unix()
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/subject'
path: '${mailbox}/subject'
metadata: subject_metadata
}
return result
}
fn (mut myvfs MailVFS) list_emails_by_type(mailbox string, list_type string, emails []mail.Email) ![]vfs.FSEntry {
mut result := []vfs.FSEntry{}
for email in emails {
if email.mailbox.split('/')[0] != mailbox {
continue
}
if list_type == 'id' {
filename := '${email.id}.json'
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/id/${filename}'
path: '${mailbox}/id/${filename}'
metadata: metadata
email: email
email: email
}
} else if list_type == 'subject' {
if envelope := email.envelope {
filename := texttools.name_fix(envelope.subject) + '.json'
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/subject/${filename}'
metadata: metadata
email: email
}
metadata := vfs.Metadata{
id: email.id
name: filename
file_type: .file
size: u64(json.encode(email).len)
created_at: email.internal_date
modified_at: email.internal_date
accessed_at: time.now().unix()
}
result << MailFSEntry{
path: '${mailbox}/subject/${filename}'
metadata: metadata
email: email
}
}
}
}
return result
}

View File

@@ -10,72 +10,72 @@ import time
fn test_mail_vfs() {
// Create a session state
mut session_state := models.new_session(name: 'test')!
// Create a mail database
mut mail_db := core.new_maildb(session_state)!
// Create some test emails
mut email1 := mail.Email{
id: 1
uid: 101
seq_num: 1
mailbox: 'Draft/important'
message: 'This is a test email 1'
id: 1
uid: 101
seq_num: 1
mailbox: 'Draft/important'
message: 'This is a test email 1'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 1'
from: ['sender1@example.com']
to: ['recipient1@example.com']
date: time.now().unix()
from: ['sender1@example.com']
to: ['recipient1@example.com']
date: time.now().unix()
}
}
mut email2 := mail.Email{
id: 2
uid: 102
seq_num: 2
mailbox: 'Draft/normal'
message: 'This is a test email 2'
id: 2
uid: 102
seq_num: 2
mailbox: 'Draft/normal'
message: 'This is a test email 2'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 2'
from: ['sender2@example.com']
to: ['recipient2@example.com']
date: time.now().unix()
from: ['sender2@example.com']
to: ['recipient2@example.com']
date: time.now().unix()
}
}
mut email3 := mail.Email{
id: 3
uid: 103
seq_num: 3
mailbox: 'Inbox'
message: 'This is a test email 3'
id: 3
uid: 103
seq_num: 3
mailbox: 'Inbox'
message: 'This is a test email 3'
internal_date: time.now().unix()
envelope: mail.Envelope{
envelope: mail.Envelope{
subject: 'Test Email 3'
from: ['sender3@example.com']
to: ['recipient3@example.com']
date: time.now().unix()
from: ['sender3@example.com']
to: ['recipient3@example.com']
date: time.now().unix()
}
}
// Add emails to the database
mail_db.set(email1) or { panic(err) }
mail_db.set(email2) or { panic(err) }
mail_db.set(email3) or { panic(err) }
// Create a mail VFS
mut mail_vfs := new(&mail_db) or { panic(err) }
// Test root directory
root := mail_vfs.root_get() or { panic(err) }
assert root.is_dir()
// Test listing mailboxes
mailboxes := mail_vfs.dir_list('') or { panic(err) }
assert mailboxes.len == 2 // Draft and Inbox
// Find the Draft mailbox
mut draft_found := false
mut inbox_found := false
@@ -89,45 +89,45 @@ fn test_mail_vfs() {
}
assert draft_found
assert inbox_found
// Test listing mailbox subdirectories
draft_subdirs := mail_vfs.dir_list('Draft') or { panic(err) }
assert draft_subdirs.len == 2 // id and subject
// Test listing emails by ID
draft_emails_by_id := mail_vfs.dir_list('Draft/id') or { panic(err) }
assert draft_emails_by_id.len == 2 // email1 and email2
// Test listing emails by subject
draft_emails_by_subject := mail_vfs.dir_list('Draft/subject') or { panic(err) }
assert draft_emails_by_subject.len == 2 // email1 and email2
// Test getting an email by ID
email1_by_id := mail_vfs.get('Draft/id/1.json') or { panic(err) }
assert email1_by_id.is_file()
// Test reading an email by ID
email1_content := mail_vfs.file_read('Draft/id/1.json') or { panic(err) }
email1_json := json.decode(mail.Email, email1_content.bytestr()) or { panic(err) }
assert email1_json.id == 1
assert email1_json.mailbox == 'Draft/important'
// // Test getting an email by subject
// email1_by_subject := mail_vfs.get('Draft/subject/Test Email 1.json') or { panic(err) }
// assert email1_by_subject.is_file()
// // Test reading an email by subject
// email1_content_by_subject := mail_vfs.file_read('Draft/subject/Test Email 1.json') or { panic(err) }
// email1_json_by_subject := json.decode(mail.Email, email1_content_by_subject.bytestr()) or { panic(err) }
// assert email1_json_by_subject.id == 1
// assert email1_json_by_subject.mailbox == 'Draft/important'
// Test exists function
assert mail_vfs.exists('Draft')
assert mail_vfs.exists('Draft/id')
assert mail_vfs.exists('Draft/id/1.json')
// assert mail_vfs.exists('Draft/subject/Test Email 1.json')
assert !mail_vfs.exists('NonExistentMailbox')
println('All mail VFS tests passed!')
}

View File

@@ -1,7 +1,7 @@
module vfs_mail
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.circles.dbs.core
import freeflowuniverse.herolib.circles.mcc.db as core
// MailVFS implements the VFS interface for mail objects
pub struct MailVFS {