diff --git a/backend/mail-plugin/src/cli.ts b/backend/mail-plugin/src/cli.ts index 73eb3c8d..b9ff6020 100644 --- a/backend/mail-plugin/src/cli.ts +++ b/backend/mail-plugin/src/cli.ts @@ -7,58 +7,65 @@ config({path: '.env.local'}); const app = express(); const port = process.env.PORT || 4000; -if (!process.env.IMAP_USER || !process.env.IMAP_PASSWORD) { - throw new Error('Provide IMAP user'); -} +app.use(async (request, response, next) => { + try { + const [user, pass] = Buffer.from(request.headers['authorization']!.replace(/^Basic /, ''), 'base64') + .toString('utf8') + .split(':'); -app.use((_request, response, next) => { - const client = new ImapFlow({ - host: 'imap.server.uni-frankfurt.de', - port: 993, - secure: true, - emitLogs: false, - auth: { - user: process.env.IMAP_USER!, - pass: process.env.IMAP_PASSWORD!, - }, - }); - response.locals.client = client; - next(); + const client = new ImapFlow({ + host: 'imap.server.uni-frankfurt.de', + port: 993, + secure: true, + emitLogs: false, + auth: {user, pass}, + }); + response.locals.client = client; + + await client.connect(); + response.on('finish', async () => { + await client.logout(); + }); + + next(); + } catch { + response.status(401).send(); + } }); -app.get('/', async (request, response) => { - const client = response.locals.client as ImapFlow; - await client.connect(); - const lock = await client.getMailboxLock('INBOX'); +app.get('/', async (_request, response) => { + const result = await response.locals.client.listTree(); + response.json(result); +}); + +app.get('/:mailbox', async (request, response) => { try { + await response.locals.client.mailboxOpen(request.params.mailbox); + const since = Number(request.query.since) || undefined; + const data = response.locals.client.fetch( + '1:*', + {}, + { + // caution, BigInt can throw + changedSince: typeof since === 'string' ? BigInt(since) : undefined, + }, + ); + const messages = []; - for await (const message of client.fetch('1:*', { - envelope: true, - labels: true, - bodyStructure: true, - flags: true, - })) { - messages.push({ - bodyStructure: message.bodyStructure, - labels: [...(message.labels ?? [])], - flags: [...(message.flags ?? [])], - envelope: message.envelope, - seq: message.seq, - }); + for await (const message of data) { + messages.push(message.seq); } response.json(messages); - } finally { - lock.release(); + } catch (error) { + console.error(error); + response.status(404).send(); } - await client.logout(); }); -app.get('/:id', async (request, response) => { - const client = response.locals.client as ImapFlow; - await client.connect(); - const lock = await client.getMailboxLock('INBOX'); +app.get('/:mailbox/:id', async (request, response) => { try { - const message = await client.fetchOne(request.params.id, { + await response.locals.client.mailboxOpen(request.params.mailbox); + const message = await response.locals.client.fetchOne(request.params.id, { envelope: true, labels: true, flags: true, @@ -71,49 +78,39 @@ app.get('/:id', async (request, response) => { envelope: message.envelope, seq: message.seq, }); - } finally { - lock.release(); + } catch (error) { + console.error(error); + response.status(404).send(); } - await client.logout(); }); -app.get('/:id/attachment/:attachment?', async (request, response) => { - const client = response.locals.client as ImapFlow; - await client.connect(); - const lock = await client.getMailboxLock('INBOX'); +app.get('/:mailbox/:id/:part', async (request, response) => { try { - const message = await client.download(request.params.id, request.params.attachment); - message.content.on('data', chunk => { - response.write(chunk); - }); - message.content.on('end', () => { + await response.locals.client.mailboxOpen(request.params.mailbox, {readOnly: true}); + if (request.query.raw) { + const message = await response.locals.client.fetchOne(request.params.id, { + bodyParts: [`${request.params.part}.mime`, request.params.part], + }); + + response.write(message.bodyParts.get(`${request.params.part}.mime`)); + response.write(message.bodyParts.get(request.params.part)); + response.end(); - }); - } finally { - lock.release(); + } else { + const message = await response.locals.client.download(request.params.id, request.params.part); + message.content.on('data', chunk => { + response.write(chunk); + }); + message.content.on('end', () => { + response.end(); + }); + } + } catch (error) { + console.error(error); + response.status(404).send(); } - await client.logout(); -}); - -app.get('/:id/raw/:part', async (request, response) => { - const client = response.locals.client as ImapFlow; - await client.connect(); - const lock = await client.getMailboxLock('INBOX'); - try { - const message = await client.fetchOne(request.params.id, { - bodyParts: [`${request.params.part}.mime`, request.params.part], - }); - - response.write(message.bodyParts.get(`${request.params.part}.mime`)); - response.write(message.bodyParts.get(request.params.part)); - - response.end(); - } finally { - lock.release(); - } - await client.logout(); }); app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); + console.log(`Server listening on port ${port}`); }); diff --git a/backend/mail-plugin/src/types.d.ts b/backend/mail-plugin/src/types.d.ts new file mode 100644 index 00000000..6badff66 --- /dev/null +++ b/backend/mail-plugin/src/types.d.ts @@ -0,0 +1,9 @@ +import {ImapFlow} from 'imapflow'; + +declare global { + namespace Express { + interface Locals { + client: ImapFlow; + } + } +} diff --git a/frontend/app/src/app/modules/mail/mail-detail.component.ts b/frontend/app/src/app/modules/mail/mail-detail.component.ts index 21961622..2f5c63a1 100644 --- a/frontend/app/src/app/modules/mail/mail-detail.component.ts +++ b/frontend/app/src/app/modules/mail/mail-detail.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'; import {MailService} from './mail.service'; import {AsyncPipe, TitleCasePipe} from '@angular/common'; import {IonicModule} from '@ionic/angular'; @@ -7,7 +7,7 @@ import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; import {UtilModule} from 'src/app/util/util.module'; import {FormatPurePipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; import {ActivatedRoute, RouterModule} from '@angular/router'; -import {mergeMap} from 'rxjs'; +import {map, mergeMap} from 'rxjs'; import {DomSanitizer} from '@angular/platform-browser'; import {materialFade} from 'src/app/animation/material-motion'; import {TranslateModule} from '@ngx-translate/core'; @@ -35,16 +35,23 @@ import {ShadowHtmlDirective} from 'src/app/util/shadow-html.directive'; ], }) export class MailDetailComponent { - mail = this.activatedRoute.params.pipe(mergeMap(({id}) => this.mailService.getEmail(id))); + readonly activatedRoute = inject(ActivatedRoute); + + readonly mailService = inject(MailService); + + readonly sanitizer = inject(DomSanitizer); + + parameters = this.activatedRoute.paramMap.pipe( + map(parameters => ({ + mailbox: parameters.get('mailbox')!, + id: parameters.get('id')!, + })), + ); + + mail = this.parameters.pipe(mergeMap(({mailbox, id}) => this.mailService.getEmail(mailbox, id))); collapse = signal(false); - constructor( - readonly mailService: MailService, - readonly activatedRoute: ActivatedRoute, - readonly sanitizer: DomSanitizer, - ) {} - todo() { alert('TODO'); } diff --git a/frontend/app/src/app/modules/mail/mail-login.component.ts b/frontend/app/src/app/modules/mail/mail-login.component.ts new file mode 100644 index 00000000..44c9fe55 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-login.component.ts @@ -0,0 +1,10 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'stapps-mail-login', + templateUrl: 'mail-login.html', + styleUrl: 'mail-login.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class MailLoginComponent {} diff --git a/frontend/app/src/app/modules/mail/mail-login.html b/frontend/app/src/app/modules/mail/mail-login.html new file mode 100644 index 00000000..e69de29b diff --git a/frontend/app/src/app/modules/mail/mail-login.scss b/frontend/app/src/app/modules/mail/mail-login.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/app/src/app/modules/mail/mail-page.component.ts b/frontend/app/src/app/modules/mail/mail-page.component.ts index c4142b09..743b2c3e 100644 --- a/frontend/app/src/app/modules/mail/mail-page.component.ts +++ b/frontend/app/src/app/modules/mail/mail-page.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; import {MailService} from './mail.service'; import {AsyncPipe} from '@angular/common'; import {IonicModule} from '@ionic/angular'; @@ -6,7 +6,8 @@ import {DataModule} from '../data/data.module'; import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; import {UtilModule} from 'src/app/util/util.module'; import {FormatPurePipeModule, IsTodayPipeModule} from 'ngx-date-fns'; -import {RouterModule} from '@angular/router'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {combineLatest, map, mergeMap} from 'rxjs'; @Component({ selector: 'stapps-mail-page', @@ -26,7 +27,19 @@ import {RouterModule} from '@angular/router'; ], }) export class MailPageComponent { - mails = this.mailService.list(); + readonly activatedRoute = inject(ActivatedRoute); - constructor(readonly mailService: MailService) {} + readonly mailService = inject(MailService); + + mailbox = this.activatedRoute.paramMap.pipe(map(parameters => parameters.get('mailbox')!)); + + mails = this.mailbox.pipe( + mergeMap(mailbox => + this.mailService + .listEmails(mailbox) + .pipe( + mergeMap(emails => combineLatest(emails.map(email => this.mailService.getEmail(mailbox, email)))), + ), + ), + ); } diff --git a/frontend/app/src/app/modules/mail/mail-page.html b/frontend/app/src/app/modules/mail/mail-page.html index 943333b8..5339393e 100644 --- a/frontend/app/src/app/modules/mail/mail-page.html +++ b/frontend/app/src/app/modules/mail/mail-page.html @@ -10,25 +10,28 @@ @if (mails | async; as mails) { @for (mail of mails; track mail) { - +
- @if (mail.envelope.from[0]; as from) { + @if (mail.from; as from) {
- {{ (from.name || from.address)?.charAt(0)?.toUpperCase() }} + {{ (from.value.name || from.value.address)?.charAt(0)?.toUpperCase() }}
}
- @for (from of mail.envelope.from; track from) { -

{{ from.name || from.address }}

+

{{ mail.from.value.name || mail.from.value.address }}

+ @if (mail.subject) { +

{{ mail.subject.value }}

} -

{{ mail.envelope.subject }}

- @if (mail.envelope.date | dfnsIsToday) { - {{ mail.envelope.date | dfnsFormatPure: 'p' }} + @if (mail.date.value | dfnsIsToday) { + {{ mail.date.value | dfnsFormatPure: 'p' }} } @else { - {{ mail.envelope.date | dfnsFormatPure: 'P' }} + {{ mail.date.value | dfnsFormatPure: 'P' }} }
diff --git a/frontend/app/src/app/modules/mail/mail-storage.provider.ts b/frontend/app/src/app/modules/mail/mail-storage.provider.ts new file mode 100644 index 00000000..85ff6433 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-storage.provider.ts @@ -0,0 +1,29 @@ +import {Injectable} from '@angular/core'; +import {StorageProvider} from '../storage/storage.provider'; +import {Email} from 'postal-mime'; + +@Injectable({providedIn: 'root'}) +export class MailStorageProvider { + constructor(readonly storageProvider: StorageProvider) {} + + private storageKey(...path: string[]): string { + return ['mail', ...path.map(encodeURIComponent)].join('/'); + } + + async get(mailbox: string, id: string): Promise { + try { + return await this.storageProvider.get(this.storageKey(mailbox, id)); + } catch (error) { + console.info('Mail not found in storage', error); + return undefined; + } + } + + async put(mailbox: string, id: string, email: Email): Promise { + await this.storageProvider.put(this.storageKey(mailbox, id), email); + } + + async delete(mailbox: string, id: string): Promise { + await this.storageProvider.delete(this.storageKey(mailbox, id)); + } +} diff --git a/frontend/app/src/app/modules/mail/mail.module.ts b/frontend/app/src/app/modules/mail/mail.module.ts index 849604a9..174136b1 100644 --- a/frontend/app/src/app/modules/mail/mail.module.ts +++ b/frontend/app/src/app/modules/mail/mail.module.ts @@ -12,16 +12,36 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {RouterModule} from '@angular/router'; -import {NgModule} from '@angular/core'; -import {MailPageComponent} from './mail-page.component'; -import {MailDetailComponent} from './mail-detail.component'; +import {RouterModule, UrlSegment, UrlSegmentGroup, UrlTree} from '@angular/router'; +import {NgModule, inject} from '@angular/core'; +import {MailService} from './mail.service'; @NgModule({ imports: [ RouterModule.forChild([ - {path: 'mail', component: MailPageComponent}, - {path: 'mail/:id', component: MailDetailComponent}, + { + path: 'mail', + loadComponent: () => import('./mail-login.component').then(m => m.MailLoginComponent), + canActivateChild: [ + () => { + if (inject(MailService).isLoggedIn()) { + return true; + } else { + return new UrlTree(new UrlSegmentGroup([new UrlSegment('/mail', {})], {})); + } + }, + ], + children: [ + { + path: ':mailbox', + loadComponent: () => import('./mail-page.component').then(m => m.MailPageComponent), + }, + { + path: ':mailbox/:id', + loadComponent: () => import('./mail-detail.component').then(m => m.MailDetailComponent), + }, + ], + }, ]), ], }) diff --git a/frontend/app/src/app/modules/mail/mail.service.ts b/frontend/app/src/app/modules/mail/mail.service.ts index 78905ac2..7d6a82d5 100644 --- a/frontend/app/src/app/modules/mail/mail.service.ts +++ b/frontend/app/src/app/modules/mail/mail.service.ts @@ -1,66 +1,86 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {Observable, map, tap, mergeMap, of, merge, forkJoin, catchError, mergeScan} from 'rxjs'; +import {Observable, map, tap, mergeMap, of, forkJoin, catchError} from 'rxjs'; import PostalMime from 'postal-mime'; import {ContentInfo, SignedData} from 'pkijs'; -import { - RawEmail, - Email, - SignedValue, - RawEmailBodyStructure, - EmailAttachment, - EmailAttachmentRemote, - Signature, -} from './schema'; +import {RawEmail, Email, SignedValue, RawEmailBodyStructure, Signature} from './schema'; import {z} from 'zod'; -import {compareAsc} from 'date-fns'; + +function value(value: undefined): undefined; +function value(value: T): SignedValue; +function value(value: T | undefined): SignedValue | undefined { + return value === undefined ? undefined : {value}; +} @Injectable({providedIn: 'root'}) export class MailService { constructor(private httpClient: HttpClient) {} - private listRawEmails(): Observable { - return this.httpClient.get('http://localhost:4000/', {responseType: 'json'}).pipe( - map(it => { - return z.array(RawEmail).parse(it); - }), - map(it => it.sort((a, b) => compareAsc(b.envelope.date, a.envelope.date))), + private request(options: { + method?: string; + path?: string[]; + options?: Record; + responseType?: 'json' | 'arraybuffer'; + }): Observable { + return this.httpClient.request( + options.method ?? 'GET', + `http://localhost:4000/${options.path?.map(encodeURIComponent).join('/') ?? ''}${ + options.options + ? `?${Object.entries(options.options).map(item => item.map(encodeURIComponent).join('='))}` + : '' + }`, + { + responseType: options.responseType as 'json', + headers: {authorization: `Basic ${btoa('test:123')}`}, + }, + ); + } + + isLoggedIn() { + return false; + } + + listMailboxes(): Observable { + return this.request({}); + } + + private listRawEmails(mailbox: string, since?: Date): Observable { + return this.request({ + path: [mailbox], + options: since !== undefined ? {since: since.valueOf().toString()} : undefined, + }).pipe( + mergeMap(it => z.array(z.string()).parseAsync(it)), tap(console.log), ); } - private getRawEmail(id: string): Observable { - return this.httpClient - .get(`http://localhost:4000/${id}`, {responseType: 'json'}) - .pipe(mergeMap(it => RawEmail.parseAsync(it))); + private getRawEmail(mailbox: string, id: string): Observable { + return this.request({ + path: [mailbox, id], + options: {raw: 'true'}, + }).pipe(mergeMap(it => RawEmail.parseAsync(it))); } - private getFullAttachment(id: string | number, attachment: string): Observable { - return this.httpClient.get(`http://localhost:4000/${id}/raw/${attachment}`, { + private getPart(mailbox: string, id: string, part: string): Observable { + return this.request({path: [mailbox, id, part], responseType: 'arraybuffer'}); + } + + private getRawPart(mailbox: string, id: string, part: string): Observable { + return this.request({ + path: [mailbox, id, part], + options: {raw: 'true'}, responseType: 'arraybuffer', }); } - private getRawAttachment(id: string | number, attachment = ''): Observable { - return this.httpClient.get(`http://localhost:4000/${id}/attachment/${attachment}`, { - responseType: 'arraybuffer', - }); - } - - private resolveRawEmail(email: RawEmail): Observable { + private resolveRawEmail(mailbox: string, email: RawEmail): Observable { console.log(email); - function value(value: undefined): undefined; - function value(value: T): SignedValue; - function value(value: T | undefined): SignedValue | undefined { - return value === undefined ? undefined : {value}; - } - if ( email.bodyStructure.type === 'application/x-pkcs7-mime' || email.bodyStructure.type === 'application/pkcs7-mime' ) { - return this.getRawAttachment(email.seq, email.bodyStructure.part ?? 'TEXT').pipe( + return this.getRawPart(mailbox, email.seq, email.bodyStructure.part ?? 'TEXT').pipe( mergeMap(async buffer => { const info = ContentInfo.fromBER(buffer); const signedData = new SignedData({schema: info.content}); @@ -87,7 +107,9 @@ export class MailService { } const result: Email = { + id: email.seq, subject: signedEmail.subject ? signed(signedEmail.subject) : value(email.envelope.subject), + flags: new Set(), //TODO from: signed({ name: signedEmail.from.name || undefined, address: signedEmail.from.address || undefined, @@ -117,8 +139,8 @@ export class MailService { // https://datatracker.ietf.org/doc/html/rfc1847#section-2.1 if (item.type === 'multipart/signed' && item.parameters?.protocol === 'application/pkcs7-signature') { return forkJoin({ - data: this.getFullAttachment(email.seq, item.childNodes![0].part!), - signature: this.getRawAttachment(email.seq, item.childNodes![1].part!), + data: this.getPart(mailbox, email.seq, item.childNodes![0].part!), + signature: this.getRawPart(mailbox, email.seq, item.childNodes![1].part!), }).pipe( mergeMap(({data, signature}) => { const info = ContentInfo.fromBER(signature); @@ -136,23 +158,23 @@ export class MailService { map(children => children[0]), ); } else if (item.type === 'text/plain') { - return this.getRawAttachment(email.seq, item.part ?? 'TEXT').pipe( + return this.getRawPart(mailbox, email.seq, item.part ?? 'TEXT').pipe( map(text => { result.html = {value: new TextDecoder().decode(text), signature}; return result; }), ); } else if (item.type === 'text/html') { - return this.getRawAttachment(email.seq, item.part ?? 'TEXT').pipe( + return this.getRawPart(mailbox, email.seq, item.part ?? 'TEXT').pipe( map(html => { result.html = {value: new TextDecoder().decode(html), signature}; return result; }), ); - } else if (item.part !== undefined) { - result.attachments.push({value: {part: item.part, size: item.size ?? NaN, filename: ''}}); + } else if (item.part === undefined) { return of(result); } else { + result.attachments.push({value: {part: item.part, size: item.size ?? Number.NaN, filename: ''}}); return of(result); } }; @@ -171,87 +193,14 @@ export class MailService { ); } - getEmail(id: string): Observable { - return this.getRawEmail(id).pipe( - mergeMap(it => this.resolveRawEmail(it)), + getEmail(mailbox: string, id: string): Observable { + return this.getRawEmail(mailbox, id).pipe( + mergeMap(it => this.resolveRawEmail(mailbox, it)), tap(console.log), ); } - list() { - return this.listRawEmails().pipe( - tap(it => { - const email = it[7]; - console.log(email); - }), - ); + listEmails(mailbox: string): Observable { + return this.listRawEmails(mailbox); } - - /*getREmail(id: string): Observable { - return this.httpClient.get(`http://localhost:4000/${id}/attachment`, {responseType: 'arraybuffer'}).pipe( - map(buffer => new TextDecoder().decode(buffer)), - mergeMap(async content => { - const email = (await PostalMime.parse(content)) as SignedEmail; - email.signature = 'none'; - - const signedMail = email.attachments.find(attachment => - /application\/(x-)?pkcs7-mime/.test(attachment.mimeType), - ); - if (signedMail) { - const info = ContentInfo.fromBER(signedMail.content); - const signedData = new SignedData({schema: info.content}); - const valid = await signedData - .verify({ - signer: 0, - data: signedData.encapContentInfo.eContent?.valueBeforeDecodeView, - }) - .catch(() => false); - const content = new TextDecoder().decode( - signedData.encapContentInfo.eContent?.valueBeforeDecodeView, - ); - const signedEmail = (await PostalMime.parse(content)) as SignedEmail; - signedEmail.signature = valid ? 'valid' : 'invalid'; - return signedEmail; - } - - const signatureIndex = email.attachments.findIndex(attachment => - /application\/(x-)?pkcs7-signature/.test(attachment.mimeType), - ); - if (signatureIndex === -1) { - return email; - } - - email.signature = 'unsupported'; - - const signature = email.attachments.splice(signatureIndex, 1)[0]; - const info = ContentInfo.fromBER(signature.content); - const signedData = new SignedData({schema: info.content}); - - const boundary = email.headers - .find(header => header.key?.toLowerCase() === 'content-type') - ?.value?.match(/boundary=["']?([^"'\s;]+)["']?/)?.[1]; - if (boundary === undefined) { - console.warn('No boundary found'); - return email; - } - - const parts = content.split(`\r\n--${boundary}\r\n`); - if (parts.length !== 3) { - console.warn('Invalid parts', parts); - return email; - } - - const valid = await signedData - .verify({signer: 0, data: new TextEncoder().encode(parts[1])}) - .catch(error => { - console.error(error); - return false; - }); - email.signature = valid ? 'valid' : 'invalid'; - - return email; - }), - tap(console.log), - ); - }*/ } diff --git a/frontend/app/src/app/modules/mail/schema.ts b/frontend/app/src/app/modules/mail/schema.ts index 83c4d074..a5fd0a1b 100644 --- a/frontend/app/src/app/modules/mail/schema.ts +++ b/frontend/app/src/app/modules/mail/schema.ts @@ -82,6 +82,8 @@ export interface EmailAttachmentLocal extends EmailAttachmentBase { export type EmailAttachment = EmailAttachmentRemote | EmailAttachmentLocal; export interface Email { + id: string; + flags: Set; subject?: SignedValue; date: SignedValue; from: SignedValue; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84a49f7e..f675cbf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,6 +330,9 @@ importers: prettier: specifier: 3.1.1 version: 3.1.1 + swagger-jsdoc: + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.0) tsup: specifier: 6.7.0 version: 6.7.0(ts-node@10.9.2)(typescript@5.4.2) @@ -2720,6 +2723,38 @@ packages: tslib: 2.6.2 dev: false + /@apidevtools/json-schema-ref-parser@9.1.2: + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.11 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + dev: true + + /@apidevtools/openapi-schemas@2.1.0: + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + dev: true + + /@apidevtools/swagger-methods@3.0.2: + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + dev: true + + /@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.0): + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.0 + z-schema: 5.0.5 + dev: true + /@awesome-cordova-plugins/calendar@6.6.0(@awesome-cordova-plugins/core@6.6.0)(rxjs@7.8.1): resolution: {integrity: sha512-NobAl4xvmq2zBeOnLI+pqRVpC66p7OpCwd3jzrQ26h8kqhr0o5wqaNcWN6WBjmgD+/AInVnLUzsziL2QpcmD7g==} peerDependencies: @@ -5640,7 +5675,7 @@ packages: object-assign: 4.1.1 open: 8.4.0 proxy-middleware: 0.15.0 - send: 0.18.0 + send: 0.19.0 serve-index: 1.9.1 transitivePeerDependencies: - supports-color @@ -7068,6 +7103,10 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@jsdevtools/ono@7.1.3: + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + dev: true + /@leichtgewicht/ip-codec@2.0.5: resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} dev: true @@ -9997,6 +10036,10 @@ packages: set-function-length: 1.2.2 dev: true + /call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + dev: true + /callsite@1.0.0: resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} dev: true @@ -10443,6 +10486,11 @@ packages: engines: {node: '>= 6'} dev: true + /commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + dev: true + /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -13503,6 +13551,7 @@ packages: /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -15606,6 +15655,10 @@ packages: resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==} dev: true + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + /lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} dev: true @@ -15621,6 +15674,10 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + /lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + dev: true + /lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: true @@ -17354,7 +17411,6 @@ packages: /openapi-types@12.1.0: resolution: {integrity: sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==} - dev: false /opencollective-postinstall@2.0.3: resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} @@ -19162,6 +19218,27 @@ packages: transitivePeerDependencies: - supports-color + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /serialize-javascript@6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} dependencies: @@ -20330,6 +20407,30 @@ packages: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} dev: true + /swagger-jsdoc@6.2.8(openapi-types@12.1.0): + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + commander: 6.2.0 + doctrine: 3.0.0 + glob: 7.1.6 + lodash.mergewith: 4.6.2 + swagger-parser: 10.0.3(openapi-types@12.1.0) + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + dev: true + + /swagger-parser@10.0.3(openapi-types@12.1.0): + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.0) + transitivePeerDependencies: + - openapi-types + dev: true + /swiper@8.4.5: resolution: {integrity: sha512-zveyEFBBv4q1sVkbJHnuH4xCtarKieavJ4SxP0QEHvdpPLJRuD7j/Xg38IVVLbp7Db6qrPsLUePvxohYx39Agw==} engines: {node: '>= 4.7.0'} @@ -21472,6 +21573,11 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -22149,6 +22255,11 @@ packages: engines: {node: '>= 6'} dev: true + /yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + dev: true + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -22245,6 +22356,18 @@ packages: engines: {node: '>=12.20'} dev: true + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.12.0 + optionalDependencies: + commander: 9.5.0 + dev: true + /zepto@1.2.0: resolution: {integrity: sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==} dev: true