Initial commit

This commit is contained in:
root
2025-12-21 09:55:58 -05:00
committed by Sebastian Krupinski
commit 169b7b4c91
57 changed files with 10105 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
import { defineStore } from 'pinia'
import { collectionService } from '../services'
import type { CollectionInterface, CollectionCreateRequest } from '../types'
import { CollectionObject, CollectionPropertiesObject } from '../models/collection'
export const useCollectionsStore = defineStore('mail-collections', {
state: () => ({
collections: {} as Record<string, Record<string, Record<string, CollectionObject>>>,
loading: false,
error: null as string | null,
}),
actions: {
async loadCollections(sources?: any) {
this.loading = true
this.error = null
try {
const response = await collectionService.list({ sources })
// Response is already in nested object format: provider -> service -> collection
// Transform to CollectionObject instances
const transformed: Record<string, Record<string, Record<string, CollectionObject>>> = {}
for (const [providerId, providerData] of Object.entries(response)) {
transformed[providerId] = {}
for (const [serviceId, collections] of Object.entries(providerData as any)) {
transformed[providerId][serviceId] = {}
// Collections come as an object keyed by identifier
for (const [collectionId, collection] of Object.entries(collections as any)) {
// Create CollectionObject instance with provider and service set
const collectionData = {
...collection,
provider: providerId,
service: serviceId,
} as CollectionInterface
transformed[providerId][serviceId][collectionId] = new CollectionObject().fromJson(collectionData)
}
}
}
this.collections = transformed
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async getCollection(provider: string, service: string | number, collectionId: string | number) {
this.loading = true
this.error = null
try {
const response = await collectionService.fetch({
provider,
service,
collection: collectionId
})
// Create CollectionObject instance
const collectionObject = new CollectionObject().fromJson(response)
// Update in store
if (!this.collections[provider]) {
this.collections[provider] = {}
}
if (!this.collections[provider][String(service)]) {
this.collections[provider][String(service)] = {}
}
this.collections[provider][String(service)][String(collectionId)] = collectionObject
return collectionObject
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async createCollection(params: {
provider: string
service: string | number
collection?: string | number | null
properties: CollectionPropertiesObject
}): Promise<CollectionObject> {
this.loading = true
this.error = null
try {
// Prepare request data from CollectionPropertiesObject
const requestData: CollectionCreateRequest = {
provider: params.provider,
service: params.service,
collection: params.collection ?? null,
properties: {
'@type': 'mail.collection',
label: params.properties.label,
role: params.properties.role ?? null,
rank: params.properties.rank ?? 0,
subscribed: params.properties.subscribed ?? true,
},
}
// Call service to create collection
const response = await collectionService.create(requestData)
// Create CollectionObject instance
const collectionObject = new CollectionObject().fromJson(response)
// Update store with new collection
const provider = response.provider
const service = String(response.service)
const identifier = String(response.identifier)
if (!this.collections[provider]) {
this.collections[provider] = {}
}
if (!this.collections[provider][service]) {
this.collections[provider][service] = {}
}
this.collections[provider][service][identifier] = collectionObject
return collectionObject
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
},
getters: {
collectionList: (state) => {
const list: CollectionObject[] = []
Object.values(state.collections).forEach(providerCollections => {
Object.values(providerCollections).forEach(serviceCollections => {
Object.values(serviceCollections).forEach(collection => {
list.push(collection)
})
})
})
return list
},
collectionCount: (state) => {
let count = 0
Object.values(state.collections).forEach(providerCollections => {
Object.values(providerCollections).forEach(serviceCollections => {
count += Object.keys(serviceCollections).length
})
})
return count
},
hasCollections: (state) => {
return Object.values(state.collections).some(providerCollections =>
Object.values(providerCollections).some(serviceCollections =>
Object.keys(serviceCollections).length > 0
)
)
},
},
})
+261
View File
@@ -0,0 +1,261 @@
import { defineStore } from 'pinia'
import { entityService } from '../services'
import type { MessageObject, EntityWrapper, MessageSendRequest } from '../types'
export const useEntitiesStore = defineStore('mail-entities', {
state: () => ({
messages: {} as Record<string, Record<string, Record<string, Record<string, EntityWrapper<MessageObject>>>>>,
signatures: {} as Record<string, Record<string, Record<string, string>>>, // Track delta signatures
loading: false,
error: null as string | null,
}),
actions: {
async loadMessages(sources?: any, filter?: any, sort?: any, range?: any) {
this.loading = true
this.error = null
try {
const response = await entityService.list({ sources, filter, sort, range })
// Entities come as objects keyed by identifier
Object.entries(response).forEach(([provider, providerData]) => {
Object.entries(providerData).forEach(([service, serviceData]) => {
Object.entries(serviceData).forEach(([collection, entities]) => {
if (!this.messages[provider]) {
this.messages[provider] = {}
}
if (!this.messages[provider][service]) {
this.messages[provider][service] = {}
}
if (!this.messages[provider][service][collection]) {
this.messages[provider][service][collection] = {}
}
// Entities are already keyed by identifier
this.messages[provider][service][collection] = entities as Record<string, EntityWrapper<MessageObject>>
})
})
})
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async getMessages(
provider: string,
service: string | number,
collection: string | number,
identifiers: (string | number)[],
properties?: string[]
) {
this.loading = true
this.error = null
try {
const response = await entityService.fetch({
provider,
service,
collection,
identifiers,
properties
})
// Update in store
if (!this.messages[provider]) {
this.messages[provider] = {}
}
if (!this.messages[provider][String(service)]) {
this.messages[provider][String(service)] = {}
}
if (!this.messages[provider][String(service)][String(collection)]) {
this.messages[provider][String(service)][String(collection)] = {}
}
// Index fetched entities by identifier
response.entities.forEach((entity: EntityWrapper<MessageObject>) => {
this.messages[provider][String(service)][String(collection)][entity.identifier] = entity
})
return response
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async searchMessages(
provider: string,
service: string | number,
query: string,
collections?: (string | number)[],
filter?: any,
sort?: any,
range?: any
) {
this.loading = true
this.error = null
try {
const response = await entityService.search({
provider,
service,
query,
collections,
filter,
sort,
range
})
return response
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async sendMessage(request: MessageSendRequest) {
this.loading = true
this.error = null
try {
const response = await entityService.send(request)
return response
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async getDelta(sources: any) {
this.loading = true
this.error = null
try {
// Sources are already in correct format: { provider: { service: { collection: signature } } }
const response = await entityService.delta({ sources })
// Process delta and update store
Object.entries(response).forEach(([provider, providerData]) => {
Object.entries(providerData).forEach(([service, serviceData]) => {
Object.entries(serviceData).forEach(([collection, collectionData]) => {
// Skip if no changes (server returns false or string signature)
if (collectionData === false || typeof collectionData === 'string') {
return
}
if (!this.messages[provider]) {
this.messages[provider] = {}
}
if (!this.messages[provider][service]) {
this.messages[provider][service] = {}
}
if (!this.messages[provider][service][collection]) {
this.messages[provider][service][collection] = {}
}
const collectionMessages = this.messages[provider][service][collection]
// Update signature if provided
if (typeof collectionData === 'object' && collectionData.signature) {
if (!this.signatures[provider]) {
this.signatures[provider] = {}
}
if (!this.signatures[provider][service]) {
this.signatures[provider][service] = {}
}
this.signatures[provider][service][collection] = collectionData.signature
console.log(`[Store] Updated signature for ${provider}/${service}/${collection}: "${collectionData.signature}"`)
}
// Process additions (from delta response format)
if (collectionData.additions) {
// Note: additions are just identifiers, need to fetch full entities separately
// This is handled by the sync composable
}
// Process modifications
if (collectionData.modifications) {
// Note: modifications are just identifiers, need to fetch full entities separately
}
// Remove deleted messages
if (collectionData.deletions) {
collectionData.deletions.forEach((id: string | number) => {
delete collectionMessages[String(id)]
})
}
// Legacy support: Also handle created/modified/deleted format
if (collectionData.created) {
collectionData.created.forEach((entity: EntityWrapper<MessageObject>) => {
collectionMessages[entity.identifier] = entity
})
}
if (collectionData.modified) {
collectionData.modified.forEach((entity: EntityWrapper<MessageObject>) => {
collectionMessages[entity.identifier] = entity
})
}
if (collectionData.deleted) {
collectionData.deleted.forEach((id: string | number) => {
delete collectionMessages[String(id)]
})
}
})
})
})
return response
} catch (error: any) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
},
getters: {
messageList: (state) => {
const list: EntityWrapper<MessageObject>[] = []
Object.values(state.messages).forEach(providerMessages => {
Object.values(providerMessages).forEach(serviceMessages => {
Object.values(serviceMessages).forEach(collectionMessages => {
Object.values(collectionMessages).forEach(message => {
list.push(message)
})
})
})
})
return list
},
messageCount: (state) => {
let count = 0
Object.values(state.messages).forEach(providerMessages => {
Object.values(providerMessages).forEach(serviceMessages => {
Object.values(serviceMessages).forEach(collectionMessages => {
count += Object.keys(collectionMessages).length
})
})
})
return count
},
hasMessages: (state) => {
return Object.values(state.messages).some(providerMessages =>
Object.values(providerMessages).some(serviceMessages =>
Object.values(serviceMessages).some(collectionMessages =>
Object.keys(collectionMessages).length > 0
)
)
)
},
},
})
+108
View File
@@ -0,0 +1,108 @@
/**
* Providers Store
*/
import { ref, computed, readonly } from 'vue'
import { defineStore } from 'pinia'
import { providerService } from '../services'
import { ProviderObject } from '../models/provider'
import type { SourceSelector } from '../types'
export const useProvidersStore = defineStore('mailProvidersStore', () => {
// State
const _providers = ref<Record<string, ProviderObject>>({})
const transceiving = ref(false)
const error = ref<string | null>(null)
// Getters
const count = computed(() => Object.keys(_providers.value).length)
const has = computed(() => count.value > 0)
/**
* Get providers as an array
* @returns Array of provider objects
*/
const providers = computed(() => Object.values(_providers.value))
/**
* Get a specific provider by identifier from cache
* @param identifier - Provider identifier
* @returns Provider object or null
*/
function provider(identifier: string): ProviderObject | null {
return _providers.value[identifier] || null
}
// Actions
/**
* Retrieve all or specific providers
*/
async function list(sources?: SourceSelector): Promise<Record<string, ProviderObject>> {
transceiving.value = true
error.value = null
try {
const response = await providerService.list({ sources })
console.debug('[Mail Manager](Store) - Successfully retrieved', Object.keys(response).length, 'providers')
_providers.value = response
return response
} catch (err: any) {
console.error('[Mail Manager](Store) - Failed to retrieve providers:', err)
error.value = err.message
throw err
} finally {
transceiving.value = false
}
}
/**
* Fetch a specific provider
*/
async function fetch(identifier: string): Promise<ProviderObject> {
transceiving.value = true
try {
return await providerService.fetch({ identifier })
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to fetch provider:', error)
throw error
} finally {
transceiving.value = false
}
}
/**
* Check which providers exist/are available
*/
async function extant(sources: SourceSelector) {
transceiving.value = true
error.value = null
try {
const response = await providerService.extant({ sources })
console.debug('[Mail Manager](Store) - Successfully checked', sources ? Object.keys(sources).length : 0, 'providers')
return response
} catch (err: any) {
console.error('[Mail Manager](Store) - Failed to check providers:', err)
error.value = err.message
throw err
} finally {
transceiving.value = false
}
}
// Return public API
return {
// State
transceiving: readonly(transceiving),
error: readonly(error),
// computed
count,
has,
providers,
provider,
// functions
list,
fetch,
extant,
}
})
+209
View File
@@ -0,0 +1,209 @@
/**
* Services Store
*/
import { ref, computed, readonly } from 'vue'
import { defineStore } from 'pinia'
import { serviceService } from '../services'
import { ServiceObject } from '../models/service'
import type {
ServiceLocation,
SourceSelector,
ServiceIdentity,
} from '../types'
export const useServicesStore = defineStore('mailServicesStore', () => {
// State
const _services = ref<Record<string, ServiceObject>>({})
const transceiving = ref(false)
const lastTestResult = ref<any>(null)
// Getters
const count = computed(() => Object.keys(_services.value).length)
const has = computed(() => count.value > 0)
/**
* Get services as an array
* @returns Array of service objects
*/
const services = computed(() => Object.values(_services.value))
/**
* Get services grouped by provider
* @returns Services grouped by provider ID
*/
const servicesByProvider = computed(() => {
const groups: Record<string, ServiceObject[]> = {}
Object.values(_services.value).forEach((service) => {
if (!groups[service.provider]) {
groups[service.provider] = []
}
groups[service.provider].push(service)
})
return groups
})
// Actions
/**
* Retrieve for all or specific services
*/
async function list(sources?: SourceSelector): Promise<Record<string, ServiceObject>> {
transceiving.value = true
try {
const response = await serviceService.list({ sources })
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
const flattened: Record<string, ServiceObject> = {}
Object.entries(response).forEach(([_providerId, providerServices]) => {
Object.entries(providerServices).forEach(([_serviceId, serviceObj]) => {
const key = `${serviceObj.provider}:${serviceObj.identifier}`
flattened[key] = serviceObj
})
})
console.debug('[Mail Manager](Store) - Successfully retrieved', Object.keys(flattened).length, 'services')
_services.value = flattened
return flattened
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to retrieve services:', error)
throw error
} finally {
transceiving.value = false
}
}
/**
* Fetch a specific service
*/
async function fetch(provider: string, identifier: string | number): Promise<ServiceObject> {
transceiving.value = true
try {
return await serviceService.fetch({ provider, identifier })
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to fetch service:', error)
throw error
} finally {
transceiving.value = false
}
}
/**
* Discover service configuration
*
* @returns Array of discovered services sorted by provider
*/
async function discover(
identity: string,
secret: string | undefined,
location: string | undefined,
provider: string | undefined,
): Promise<ServiceObject[]> {
transceiving.value = true
try {
const services = await serviceService.discover({identity, secret, location, provider})
console.debug('[Mail Manager](Store) - Successfully discovered', services.length, 'services')
return services
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to discover service:', error)
throw error
} finally {
transceiving.value = false
}
}
async function test(
provider: string,
identifier?: string | number | null,
location?: ServiceLocation | null,
identity?: ServiceIdentity | null,
): Promise<any> {
transceiving.value = true
try {
const response = await serviceService.test({ provider, identifier, location, identity })
lastTestResult.value = response
return response
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to test service:', error)
throw error
} finally {
transceiving.value = false
}
}
async function create(provider: string, data: any) {
transceiving.value = true
try {
const serviceObj = await serviceService.create({ provider, data })
// Add to store with composite key
const key = `${serviceObj.provider}:${serviceObj.identifier}`
_services.value[key] = serviceObj
return serviceObj
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to create service:', error)
throw error
} finally {
transceiving.value = false
}
}
async function update(provider: string, identifier: string | number, data: any) {
transceiving.value = true
try {
const serviceObj = await serviceService.update({ provider, identifier, data })
// Update in store with composite key
const key = `${serviceObj.provider}:${serviceObj.identifier}`
_services.value[key] = serviceObj
return serviceObj
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to update service:', error)
throw error
} finally {
transceiving.value = false
}
}
async function remove(provider: string, identifier: string | number) {
transceiving.value = true
try {
await serviceService.delete({ provider, identifier })
// Remove from store using composite key
const key = `${provider}:${identifier}`
delete _services.value[key]
} catch (error: any) {
console.error('[Mail Manager](Store) - Failed to delete service:', error)
throw error
} finally {
transceiving.value = false
}
}
// Return public API
return {
// State (readonly)
transceiving: readonly(transceiving),
lastTestResult: readonly(lastTestResult),
// Getters
count,
has,
services,
servicesByProvider,
// Actions
list,
fetch,
discover,
test,
create,
update,
delete: remove,
}
})