Skip to content

Mail Inbox ​

Needs an access token

Demonstrates character mail operations: reading inbox headers, viewing mail labels, listing mailing lists, and reading individual messages. Also shows the available write operations (send, delete, label management).

REQUIRES AUTHENTICATION — set ESI_ACCESS_TOKEN in your environment.

ESI Scopes Required:

  • esi-mail.read_mail.v1 (read mail headers, body, labels, mailing lists)
  • esi-mail.send_mail.v1 (send mail — shown but not executed)
  • esi-mail.organize_mail.v1 (delete mail, manage labels — shown but not executed)

Calls: mail.getMailHeaders · mail.getMailLabels · mail.getMailingLists · mail.getMail

Run it ​

bash
git clone https://github.com/lgriffin/ESI.ts.git && cd ESI.ts
npm ci
npm run example:mail

examples/mail-inbox.ts on GitHub. The imports below are written as a consumer of the package writes them; the file in the repository imports from ../src.

Source ​

ts
import { EsiClient } from '@lgriffin/esi.ts';
import { EsiError } from '@lgriffin/esi.ts/errors';
import { createConsoleLogger } from '@lgriffin/esi.ts';

// The program's own output. The client logs through the same console sink
// at ESI_LOG_LEVEL (default warn), so its diagnostics stay out of the way.
const log = createConsoleLogger('info');
const esiLog = createConsoleLogger();

const CHARACTER_ID = 1689391488;

async function main() {
  const client = new EsiClient({
    logger: esiLog,
    clientId: 'esi-ts-mail-demo',
  });

  try {
    log.info('Mail Inbox\n');

    // Fetch inbox headers, labels, and mailing lists in parallel
    // Scope: esi-mail.read_mail.v1
    log.info('Fetching mail data...');
    const [headers, labelsData, mailingLists] = await Promise.all([
      client.mail.getMailHeaders(CHARACTER_ID),
      client.mail.getMailLabels(CHARACTER_ID),
      client.mail.getMailingLists(CHARACTER_ID),
    ]);

    // Labels and unread count
    log.info('Mail Labels');
    log.info('-'.repeat(40));
    if (labelsData.total_unread_count !== undefined) {
      log.info(`  Total unread: ${labelsData.total_unread_count}`);
    }
    if (labelsData.labels && labelsData.labels.length > 0) {
      for (const label of labelsData.labels) {
        const unread = label.unread_count
          ? ` (${label.unread_count} unread)`
          : '';
        log.info(`  [${label.label_id}] ${label.name}${unread}`);
      }
    } else {
      log.info('  No custom labels');
    }

    // Mailing lists
    log.info(`\nMailing Lists (${mailingLists.length})`);
    log.info('-'.repeat(40));
    if (mailingLists.length === 0) {
      log.info('  Not subscribed to any mailing lists');
    } else {
      for (const list of mailingLists) {
        log.info(`  [${list.mailing_list_id}] ${list.name}`);
      }
    }

    // Inbox headers
    log.info(`\nInbox (${headers.length} messages)`);
    log.info('-'.repeat(40));
    if (headers.length === 0) {
      log.info('  Inbox is empty');
    } else {
      const recent = headers.slice(0, 10);
      for (const mail of recent) {
        const date = mail.timestamp
          ? new Date(mail.timestamp).toLocaleDateString()
          : 'unknown';
        const read = mail.is_read ? ' ' : '*';
        log.info(
          `  ${read} ${date} | From ${mail.from || 'unknown'} | ${mail.subject || '(no subject)'}`,
        );
      }
      if (headers.length > 10) {
        log.info(`  ... and ${headers.length - 10} more messages`);
      }

      // Read the first mail's full body
      // Scope: esi-mail.read_mail.v1
      if (recent[0]?.mail_id) {
        log.info(`\nReading mail #${recent[0].mail_id}...`);
        log.info('-'.repeat(40));
        try {
          const fullMail = await client.mail.getMail(
            CHARACTER_ID,
            recent[0].mail_id,
          );
          log.info(`  Subject: ${fullMail.subject || '(no subject)'}`);
          log.info(`  From:    ${fullMail.from}`);
          if (fullMail.body) {
            let stripped = fullMail.body;
            let prev = '';
            while (prev !== stripped) {
              prev = stripped;
              stripped = stripped.replace(/<[^>]*?>/g, '');
            }
            const preview = stripped.substring(0, 200);
            log.info(
              `  Body:    ${preview}${stripped.length > 200 ? '...' : ''}`,
            );
          }
        } catch {
          log.info('  Could not read mail body');
        }
      }
    }

    // Write operations available but not executed:
    // client.mail.sendMail(characterId, { recipients, subject, body })  — Scope: esi-mail.send_mail.v1
    // client.mail.deleteMail(characterId, mailId)                       — Scope: esi-mail.organize_mail.v1
    // client.mail.createMailLabel(characterId, { name, color })          — Scope: esi-mail.organize_mail.v1
    // client.mail.deleteMailLabel(characterId, labelId)                  — Scope: esi-mail.organize_mail.v1
    // client.mail.updateMailMetadata(characterId, mailId, { read, labels }) — Scope: esi-mail.organize_mail.v1
    log.info(
      '\nNote: write operations (sendMail, deleteMail, createMailLabel) require additional scopes',
    );
  } catch (err) {
    if (
      err instanceof EsiError &&
      (err.statusCode === 401 || err.statusCode === 403)
    ) {
      log.error(
        'Authentication required. Set ESI_ACCESS_TOKEN with scope esi-mail.read_mail.v1',
      );
    } else {
      log.error('Request failed', { error: err });
    }
    process.exit(1);
  } finally {
    await client.shutdown();
  }
}

main();

Released under the GPL-3.0-or-later license.