feat: mail

This commit is contained in:
2024-09-25 17:02:02 +02:00
committed by Thea Schöbl
parent f81690e19f
commit bc4c45ea21
13 changed files with 390 additions and 228 deletions

View File

@@ -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}`);
});

9
backend/mail-plugin/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import {ImapFlow} from 'imapflow';
declare global {
namespace Express {
interface Locals {
client: ImapFlow;
}
}
}

View File

@@ -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');
}

View File

@@ -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 {}

View File

@@ -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)))),
),
),
);
}

View File

@@ -10,25 +10,28 @@
@if (mails | async; as mails) {
<ion-list>
@for (mail of mails; track mail) {
<ion-item [routerLink]="['/mail', mail.seq]" [class.unread]="!mail.flags.has('\\Seen')">
<ion-item
[routerLink]="['/mail', mailbox | async, mail.id]"
[class.unread]="!mail.flags.has('\\Seen')"
>
<div slot="start" class="avatar">
@if (mail.envelope.from[0]; as from) {
@if (mail.from; as from) {
<div>
{{ (from.name || from.address)?.charAt(0)?.toUpperCase() }}
{{ (from.value.name || from.value.address)?.charAt(0)?.toUpperCase() }}
</div>
}
</div>
<ion-label>
@for (from of mail.envelope.from; track from) {
<h2>{{ from.name || from.address }}</h2>
<h2>{{ mail.from.value.name || mail.from.value.address }}</h2>
@if (mail.subject) {
<p>{{ mail.subject.value }}</p>
}
<p>{{ mail.envelope.subject }}</p>
</ion-label>
<ion-note slot="end">
@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' }}
}
</ion-note>
</ion-item>

View File

@@ -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<Email | undefined> {
try {
return await this.storageProvider.get<Email>(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<void> {
await this.storageProvider.put(this.storageKey(mailbox, id), email);
}
async delete(mailbox: string, id: string): Promise<void> {
await this.storageProvider.delete(this.storageKey(mailbox, id));
}
}

View File

@@ -12,16 +12,36 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
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),
},
],
},
]),
],
})

View File

@@ -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<T>(value: T): SignedValue<T>;
function value<T>(value: T | undefined): SignedValue<T> | undefined {
return value === undefined ? undefined : {value};
}
@Injectable({providedIn: 'root'})
export class MailService {
constructor(private httpClient: HttpClient) {}
private listRawEmails(): Observable<RawEmail[]> {
return this.httpClient.get('http://localhost:4000/', {responseType: 'json'}).pipe(
map<unknown, RawEmail[]>(it => {
return z.array(RawEmail).parse(it);
}),
map(it => it.sort((a, b) => compareAsc(b.envelope.date, a.envelope.date))),
private request<T>(options: {
method?: string;
path?: string[];
options?: Record<string, string>;
responseType?: 'json' | 'arraybuffer';
}): Observable<T> {
return this.httpClient.request<T>(
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<string[]> {
return this.request<string[]>({});
}
private listRawEmails(mailbox: string, since?: Date): Observable<string[]> {
return this.request<unknown>({
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<RawEmail> {
return this.httpClient
.get(`http://localhost:4000/${id}`, {responseType: 'json'})
.pipe(mergeMap(it => RawEmail.parseAsync(it)));
private getRawEmail(mailbox: string, id: string): Observable<RawEmail> {
return this.request<unknown>({
path: [mailbox, id],
options: {raw: 'true'},
}).pipe(mergeMap(it => RawEmail.parseAsync(it)));
}
private getFullAttachment(id: string | number, attachment: string): Observable<ArrayBuffer> {
return this.httpClient.get(`http://localhost:4000/${id}/raw/${attachment}`, {
private getPart(mailbox: string, id: string, part: string): Observable<ArrayBuffer> {
return this.request<ArrayBuffer>({path: [mailbox, id, part], responseType: 'arraybuffer'});
}
private getRawPart(mailbox: string, id: string, part: string): Observable<ArrayBuffer> {
return this.request({
path: [mailbox, id, part],
options: {raw: 'true'},
responseType: 'arraybuffer',
});
}
private getRawAttachment(id: string | number, attachment = ''): Observable<ArrayBuffer> {
return this.httpClient.get(`http://localhost:4000/${id}/attachment/${attachment}`, {
responseType: 'arraybuffer',
});
}
private resolveRawEmail(email: RawEmail): Observable<Email> {
private resolveRawEmail(mailbox: string, email: RawEmail): Observable<Email> {
console.log(email);
function value(value: undefined): undefined;
function value<T>(value: T): SignedValue<T>;
function value<T>(value: T | undefined): SignedValue<T> | 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<string>(), //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<Email> {
return this.getRawEmail(id).pipe(
mergeMap(it => this.resolveRawEmail(it)),
getEmail(mailbox: string, id: string): Observable<Email> {
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<string[]> {
return this.listRawEmails(mailbox);
}
/*getREmail(id: string): Observable<SignedEmail> {
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),
);
}*/
}

View File

@@ -82,6 +82,8 @@ export interface EmailAttachmentLocal extends EmailAttachmentBase {
export type EmailAttachment = EmailAttachmentRemote | EmailAttachmentLocal;
export interface Email {
id: string;
flags: Set<string>;
subject?: SignedValue<string>;
date: SignedValue<Date>;
from: SignedValue<EmailAddress>;

127
pnpm-lock.yaml generated
View File

@@ -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