Files
mail_manager/src/models/message.ts
Sebastian Krupinski c7ef2c5495
Some checks failed
Build Test / test (pull_request) Successful in 26s
JS Unit Tests / test (pull_request) Failing after 29s
PHP Unit Tests / test (pull_request) Successful in 56s
refactor: use resource identifiers
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-05-14 22:34:18 -04:00

448 lines
11 KiB
TypeScript

/**
* Message and MessagePart model classes
*/
import type {
MessageAddressInterface,
MessageInterface,
MessageModelInterface,
MessagePartInterface,
MessagePartModelInterface
} from "@/types/message";
import { clonePlain } from './clone-plain';
/**
* Message class for working with message objects
*/
export class MessageObject implements MessageModelInterface {
_data: MessageInterface;
_body: MessagePartObject | null = null;
constructor() {
this._data = {
'@type': 'mail:message',
};
this._body = null;
}
fromJson(data: MessageInterface): MessageObject {
this._data = clonePlain(data);
this._body = null;
return this;
}
toJson(): MessageInterface {
const json = this._body
? {
...this._data,
body: this._body.toJson(),
}
: this._data;
return clonePlain(json);
}
clone(): MessageObject {
return new MessageObject().fromJson(this.toJson());
}
/** Properties */
get size(): number {
return this._data.size ?? 0;
}
get headers(): Record<string, string> {
return clonePlain(this._data.headers ?? {});
}
get urid(): string | null{
return this._data.urid ?? null;
}
get inReplyTo(): string | null {
return this._data.inReplyTo ?? null;
}
get references(): string | null {
return this._data.references ?? null;
}
get received(): string | null {
return this._data.received ?? null;
}
get sent(): string | null {
return this._data.sent ?? null;
}
get sender(): MessageAddressObject | null {
return this._data.sender ? new MessageAddressObject(this._data.sender) : null;
}
get from(): MessageAddressObject | null {
return this._data.from ? new MessageAddressObject(this._data.from) : null;
}
get replyTo(): Array<MessageAddressObject> | null {
return this._data.replyTo ? this._data.replyTo.map(addr => new MessageAddressObject(addr)) : null;
}
get to(): Array<MessageAddressObject> | null {
return this._data.to ? this._data.to.map(addr => new MessageAddressObject(addr)) : null;
}
get cc(): Array<MessageAddressObject> | null {
return this._data.cc ? this._data.cc.map(addr => new MessageAddressObject(addr)) : null;
}
get bcc(): Array<MessageAddressObject> | null {
return this._data.bcc ? this._data.bcc.map(addr => new MessageAddressObject(addr)) : null;
}
get subject(): string | null {
return this._data.subject ?? null;
}
get body(): MessagePartObject | null {
if (this._body) {
return this._body;
}
else if (this._data.body) {
this._body = new MessagePartObject(this._data.body);
return this._body;
}
return null;
}
get attachments(): Array<MessagePartObject> {
return this._data.attachments ? this._data.attachments.map(att => new MessagePartObject(att)) : [];
}
get flags(): { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean } | {} {
return clonePlain(this._data.flags ?? {});
}
/** Helper methods */
get isRead(): boolean {
return this._data.flags?.read ?? false;
}
get isFlagged(): boolean {
return this._data.flags?.flagged ?? false;
}
get isAnswered(): boolean {
return this._data.flags?.answered ?? false;
}
get isDraft(): boolean {
return this._data.flags?.draft ?? false;
}
get hasAttachments(): boolean {
return (this._data.attachments?.length ?? 0) > 0;
}
hasRecipients(): boolean {
return (this._data.to?.length ?? 0) > 0
|| (this._data.cc?.length ?? 0) > 0
|| (this._data.bcc?.length ?? 0) > 0;
}
/** Body content helpers */
getBody(): MessagePartObject | null {
if (!this._body && this._data.body) {
this._body = new MessagePartObject(this._data.body);
}
return this._body;
}
hasContent(): boolean {
return !!this.getTextContent() || !!this.getHtmlContent();
}
hasTextContent(): boolean {
return !!this.getTextContent();
}
getTextContent(): string | null {
const bodyPart = this.getBody();
return bodyPart ? bodyPart.extractTextContent() : null;
}
hasHtmlContent(): boolean {
return !!this.getHtmlContent();
}
getHtmlContent(): string | null {
const bodyPart = this.getBody();
return bodyPart ? bodyPart.extractHtmlContent() : null;
}
findPartById(partId: string): MessagePartInterface | null {
const bodyPart = this.getBody();
return bodyPart ? bodyPart.findPartById(partId) : null;
}
findPartsByType(type: string): MessagePartInterface[] {
const bodyPart = this.getBody();
return bodyPart ? bodyPart.findPartsByType(type) : [];
}
}
export class MessageAddressObject implements MessageAddressInterface {
_data: MessageAddressInterface;
constructor(data: MessageAddressInterface) {
this._data = clonePlain(data);
}
fromJson(data: MessageAddressInterface): MessageAddressObject {
this._data = clonePlain(data);
return this;
}
toJson(): MessageAddressInterface {
return clonePlain(this._data);
}
clone(): MessageAddressObject {
return new MessageAddressObject(this.toJson());
}
/** Properties */
get address(): string {
return this._data.address;
}
get label(): string | undefined {
return this._data.label;
}
}
/**
* MessagePart class for working with message body parts
*/
export class MessagePartObject implements MessagePartModelInterface {
_data: MessagePartInterface;
_subParts: MessagePartObject[] = [];
constructor(data?: Partial<MessagePartInterface>) {
this._data = {
partId: clonePlain(data?.partId ?? null),
blobId: clonePlain(data?.blobId ?? null),
size: clonePlain(data?.size ?? null),
name: clonePlain(data?.name ?? null),
type: clonePlain(data?.type ?? null),
charset: clonePlain(data?.charset ?? null),
disposition: clonePlain(data?.disposition ?? null),
cid: clonePlain(data?.cid ?? null),
language: clonePlain(data?.language ?? null),
location: clonePlain(data?.location ?? null),
content: clonePlain(data?.content ?? null),
subParts: clonePlain(data?.subParts ?? []),
};
}
fromJson(data: MessagePartInterface): MessagePartObject {
this._data = clonePlain(data);
this._subParts = [];
return this;
}
toJson(): MessagePartInterface {
const json = this._subParts.length > 0
? {
...this._data,
subParts: this._subParts.map(subPart => subPart.toJson()),
}
: this._data;
return clonePlain(json);
}
clone(): MessagePartObject {
return new MessagePartObject().fromJson(this.toJson());
}
/** Properties */
get partId(): string | null {
return this._data.partId ?? null;
}
get blobId(): string | null {
return this._data.blobId ?? null;
}
get size(): number | null {
return this._data.size ?? null;
}
get name(): string | null {
return this._data.name ?? null;
}
get type(): string | null {
return this._data.type ?? null;
}
get charset(): string | null {
return this._data.charset ?? null;
}
get disposition(): string | null {
return this._data.disposition ?? null;
}
get cid(): string | null {
return this._data.cid ?? null;
}
get language(): string | null {
return this._data.language ?? null;
}
get location(): string | null {
return this._data.location ?? null;
}
get content(): string | null {
return this._data.content ?? null;
}
get subParts(): MessagePartModelInterface[] {
if (this._subParts) {
return this._subParts;
}
else if (this._data.subParts) {
this._subParts = this._data.subParts.map((subPart) => new MessagePartObject(subPart));
return this._subParts;
}
return [];
}
/** Helper methods */
hasContent(): boolean {
return !!this._data.content;
}
hasSubParts(): boolean {
return this.subParts.length > 0;
}
isMultipart(): boolean {
return this._data.type?.startsWith('multipart/') ?? false;
}
isText(): boolean {
return this._data.type === 'text/plain';
}
isHtml(): boolean {
return this._data.type === 'text/html';
}
isAttachment(): boolean {
return this._data.disposition === 'attachment';
}
isInline(): boolean {
return this._data.disposition === 'inline';
}
/**
* Find a part by partId (recursive search)
*/
findPartById(partId: string): MessagePartInterface | null {
if (this._data.partId === partId) {
return this._data;
}
if (this._data.subParts) {
for (const subPart of this._data.subParts) {
const part = new MessagePartObject(subPart);
const found = part.findPartById(partId);
if (found) {
return found;
}
}
}
return null;
}
/**
* Find all parts of a specific type (recursive search)
*/
findPartsByType(type: string): MessagePartInterface[] {
const parts: MessagePartInterface[] = [];
if (this._data.type === type) {
parts.push(this._data);
}
if (this._data.subParts) {
for (const subPart of this._data.subParts) {
const part = new MessagePartObject(subPart);
parts.push(...part.findPartsByType(type));
}
}
return parts;
}
/**
* Extract text content from body structure
*/
extractTextContent(): string | null {
if (this._data.type === 'text/plain' && this._data.content) {
return this._data.content;
}
if (this._data.subParts) {
for (const subPart of this._data.subParts) {
const part = new MessagePartObject(subPart);
const content = part.extractTextContent();
if (content) {
return content;
}
}
}
return null;
}
/**
* Extract HTML content from body structure
*/
extractHtmlContent(): string | null {
if (this._data.type === 'text/html' && this._data.content) {
return this._data.content;
}
if (this._data.subParts) {
for (const subPart of this._data.subParts) {
const part = new MessagePartObject(subPart);
const content = part.extractHtmlContent();
if (content) {
return content;
}
}
}
return null;
}
}