
Imap Smtp Email
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
imap-smtp-email is a Claude skill that reads, searches, and sends email over IMAP/SMTP with multi-account and attachment support.
About
This skill reads, searches, and manages email over IMAP and sends email over SMTP, with attachment support. It supports multiple accounts via prefixed environment variables and works with Gmail, Outlook, and several Chinese mail providers. Setup is script-driven and includes a file-access whitelist.
- Reads, searches, and manages email over IMAP and sends over SMTP
- Supports multiple accounts and attachments, including Gmail, Outlook, and 163/126/188.com
- Ships CLI scripts with a file-access whitelist for security
Imap Smtp Email by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,522 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
imap-smtp-email capabilities & compatibility
Free tool; requires your own IMAP/SMTP credentials (e.g. a Gmail App Password).
- Capabilities
- read email · send email · search mailbox · download attachments
- Works with
- gmail · outlook
- Use cases
- Runs
- Runs locally
- Pricing
- Bring your own API key
What imap-smtp-email says it does
Read, search, and manage email via IMAP protocol. Send email via SMTP.
Gmail does **not** accept your regular account password
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill imap-smtp-emailAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Read, search, send, and manage email across IMAP/SMTP accounts from the command line.
Who is it for?
Developers automating inbox checks and email sending across multiple IMAP/SMTP accounts.
Skip if: Providers that do not expose IMAP/SMTP access.
When should I use this skill?
You need to check, search, fetch, or send email from IMAP/SMTP accounts programmatically.
By the numbers
- multi-account via uppercase env prefixes
- documents 9+ common IMAP/SMTP providers
Files
IMAP/SMTP Email Tool
Read, search, and manage email via IMAP protocol. Send email via SMTP. Supports Gmail, Outlook, 163.com, vip.163.com, 126.com, vip.126.com, 188.com, vip.188.com, and any standard IMAP/SMTP server.
Configuration
Run the setup script to configure your email account:
bash setup.shConfiguration is stored at ~/.config/imap-smtp-email/.env (survives skill updates). If no config is found there, the skill falls back to a .env file in the skill directory (for backward compatibility).
Config file format
# Default account (no prefix)
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_USER=your@email.com
IMAP_PASS=your_password
IMAP_TLS=true
IMAP_REJECT_UNAUTHORIZED=true
IMAP_MAILBOX=INBOX
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your@email.com
SMTP_PASS=your_password
SMTP_FROM=your@email.com
SMTP_REJECT_UNAUTHORIZED=true
# File access whitelist (security)
ALLOWED_READ_DIRS=~/Downloads,~/Documents
ALLOWED_WRITE_DIRS=~/DownloadsMulti-Account
You can configure additional email accounts in the same config file. Each account uses a name prefix (uppercase) on all variables.
Adding an account
Run the setup script and choose "Add a new account":
bash setup.shOr manually add prefixed variables to ~/.config/imap-smtp-email/.env:
# Work account (WORK_ prefix)
WORK_IMAP_HOST=imap.company.com
WORK_IMAP_PORT=993
WORK_IMAP_USER=me@company.com
WORK_IMAP_PASS=password
WORK_IMAP_TLS=true
WORK_IMAP_REJECT_UNAUTHORIZED=true
WORK_IMAP_MAILBOX=INBOX
WORK_SMTP_HOST=smtp.company.com
WORK_SMTP_PORT=587
WORK_SMTP_SECURE=false
WORK_SMTP_USER=me@company.com
WORK_SMTP_PASS=password
WORK_SMTP_FROM=me@company.com
WORK_SMTP_REJECT_UNAUTHORIZED=trueUsing a named account
Add --account <name> before the command:
node scripts/imap.js --account work check
node scripts/smtp.js --account work send --to foo@bar.com --subject Hi --body HelloWithout --account, the default (unprefixed) account is used.
Account name rules
- Letters and digits only (e.g.,
work,163,personal2) - Case-insensitive:
workandWORKrefer to the same account - The prefix in
.envis always uppercase (e.g.,WORK_IMAP_HOST) ALLOWED_READ_DIRSandALLOWED_WRITE_DIRSare shared across all accounts (always unprefixed)
Common Email Servers
| Provider | IMAP Host | IMAP Port | SMTP Host | SMTP Port |
|---|---|---|---|---|
| 163.com | imap.163.com | 993 | smtp.163.com | 465 |
| vip.163.com | imap.vip.163.com | 993 | smtp.vip.163.com | 465 |
| 126.com | imap.126.com | 993 | smtp.126.com | 465 |
| vip.126.com | imap.vip.126.com | 993 | smtp.vip.126.com | 465 |
| 188.com | imap.188.com | 993 | smtp.188.com | 465 |
| vip.188.com | imap.vip.188.com | 993 | smtp.vip.188.com | 465 |
| yeah.net | imap.yeah.net | 993 | smtp.yeah.net | 465 |
| Gmail | imap.gmail.com | 993 | smtp.gmail.com | 587 |
| Outlook | outlook.office365.com | 993 | smtp.office365.com | 587 |
| QQ Mail | imap.qq.com | 993 | smtp.qq.com | 587 |
Important for Gmail:
- Gmail does not accept your regular account password
- You must generate an App Password: https://myaccount.google.com/apppasswords
- Use the generated 16-character App Password as
IMAP_PASS/SMTP_PASS - Requires Google Account with 2-Step Verification enabled
Important for 163.com:
- Use authorization code (授权码), not account password
- Enable IMAP/SMTP in web settings first
IMAP Commands (Receiving Email)
check
Check for new/unread emails.
node scripts/imap.js [--account <name>] check [--limit 10] [--mailbox INBOX] [--recent 2h]Options:
--limit <n>: Max results (default: 10)--mailbox <name>: Mailbox to check (default: INBOX)--recent <time>: Only show emails from last X time (e.g., 30m, 2h, 7d)
fetch
Fetch full email content by UID.
node scripts/imap.js [--account <name>] fetch <uid> [--mailbox INBOX]download
Download all attachments from an email, or a specific attachment.
node scripts/imap.js [--account <name>] download <uid> [--mailbox INBOX] [--dir <path>] [--file <filename>]Options:
--mailbox <name>: Mailbox (default: INBOX)--dir <path>: Output directory (default: current directory)--file <filename>: Download only the specified attachment (default: download all)
search
Search emails with filters.
node scripts/imap.js [--account <name>] search [options]
Options:
--unseen Only unread messages
--seen Only read messages
--from <email> From address contains
--subject <text> Subject contains
--recent <time> From last X time (e.g., 30m, 2h, 7d)
--since <date> After date (YYYY-MM-DD)
--before <date> Before date (YYYY-MM-DD)
--limit <n> Max results (default: 20)
--mailbox <name> Mailbox to search (default: INBOX)mark-read / mark-unread
Mark message(s) as read or unread.
node scripts/imap.js [--account <name>] mark-read <uid> [uid2 uid3...]
node scripts/imap.js [--account <name>] mark-unread <uid> [uid2 uid3...]list-mailboxes
List all available mailboxes/folders.
node scripts/imap.js [--account <name>] list-mailboxeslist-accounts
List all configured email accounts.
node scripts/imap.js list-accounts
node scripts/smtp.js list-accountsShows account name, email address, server addresses, and configuration status.
SMTP Commands (Sending Email)
send
Send email via SMTP.
node scripts/smtp.js [--account <name>] send --to <email> --subject <text> [options]Required:
--to <email>: Recipient (comma-separated for multiple)--subject <text>: Email subject, or--subject-file <file>
Optional:
--body <text>: Plain text body--html: Send body as HTML--body-file <file>: Read body from file--html-file <file>: Read HTML from file--cc <email>: CC recipients--bcc <email>: BCC recipients--attach <file>: Attachments (comma-separated)--from <email>: Override default sender
Examples:
# Simple text email
node scripts/smtp.js send --to recipient@example.com --subject "Hello" --body "World"
# HTML email
node scripts/smtp.js send --to recipient@example.com --subject "Newsletter" --html --body "<h1>Welcome</h1>"
# Email with attachment
node scripts/smtp.js send --to recipient@example.com --subject "Report" --body "Please find attached" --attach report.pdf
# Multiple recipients
node scripts/smtp.js send --to "a@example.com,b@example.com" --cc "c@example.com" --subject "Update" --body "Team update"test
Test SMTP connection by sending a test email to yourself.
node scripts/smtp.js [--account <name>] testDependencies
npm installSecurity Notes
- Configuration is stored at
~/.config/imap-smtp-email/.envwith600permissions (owner read/write only) - Gmail: regular password is rejected — generate an App Password at https://myaccount.google.com/apppasswords
- For 163.com: use authorization code (授权码), not account password
Troubleshooting
Connection timeout:
- Verify server is running and accessible
- Check host/port configuration
Authentication failed:
- Verify username (usually full email address)
- Check password is correct
- For 163.com: use authorization code, not account password
- For Gmail: regular password won't work — generate an App Password at https://myaccount.google.com/apppasswords
TLS/SSL errors:
- Match
IMAP_TLS/SMTP_SECUREsetting to server requirements - For self-signed certs: set
IMAP_REJECT_UNAUTHORIZED=falseorSMTP_REJECT_UNAUTHORIZED=false
{
"name": "imap-smtp-email-skill",
"version": "1.0.0",
"description": "IMAP/SMTP email tool for Claude. Works with Gmail, Outlook, 163.com, vip.163.com, 126.com, vip.126.com, 188.com, vip.188.com, and any standard IMAP/SMTP server.",
"main": "scripts/imap.js",
"scripts": {
"check": "node scripts/imap.js check",
"fetch": "node scripts/imap.js fetch",
"search": "node scripts/imap.js search"
},
"dependencies": {
"dotenv": "^16.6.1",
"imap": "^0.8.19",
"imap-simple": "^5.1.0",
"mailparser": "^3.9.3",
"nodemailer": "^7.0.13"
},
"keywords": [
"imap",
"smtp",
"email",
"163.com",
"126.com",
"188.com",
"gmail",
"outlook",
"skill"
],
"author": "NetEase",
"license": "MIT"
}
#!/usr/bin/env node
const path = require('path');
const os = require('os');
const fs = require('fs');
const dotenv = require('dotenv');
// Config file locations
const PRIMARY_ENV_PATH = path.join(os.homedir(), '.config', 'imap-smtp-email', '.env');
const FALLBACK_ENV_PATH = path.resolve(__dirname, '../.env');
// Find the .env file: primary location first, then fallback
function findEnvPath() {
if (fs.existsSync(PRIMARY_ENV_PATH)) return PRIMARY_ENV_PATH;
if (fs.existsSync(FALLBACK_ENV_PATH)) return FALLBACK_ENV_PATH;
return null;
}
// Parse and strip --account <name> from process.argv
// After this, process.argv[2] is always the command
function parseAccountFromArgv(argv) {
const args = argv.slice(2);
const idx = args.indexOf('--account');
if (idx !== -1 && idx + 1 < args.length) {
const name = args[idx + 1];
args.splice(idx, 2);
return { accountName: name, remainingArgs: args };
}
return { accountName: null, remainingArgs: args };
}
// Build config object from environment variables
// prefix: uppercase account name (e.g., 'WORK') or null for default
function buildConfig(env, prefix) {
const p = prefix ? `${prefix}_` : '';
// Account existence check for named accounts
if (prefix && !env[`${p}IMAP_HOST`]) {
console.error(`Error: Account "${prefix.toLowerCase()}" not found in config. Check ~/.config/imap-smtp-email/.env`);
process.exit(1);
}
return {
imap: {
host: env[`${p}IMAP_HOST`] || '127.0.0.1',
port: parseInt(env[`${p}IMAP_PORT`]) || 1143,
user: env[`${p}IMAP_USER`],
pass: env[`${p}IMAP_PASS`],
tls: env[`${p}IMAP_TLS`] === 'true',
rejectUnauthorized: env[`${p}IMAP_REJECT_UNAUTHORIZED`] !== 'false',
mailbox: env[`${p}IMAP_MAILBOX`] || 'INBOX',
},
smtp: {
host: env[`${p}SMTP_HOST`],
port: parseInt(env[`${p}SMTP_PORT`]) || 587,
user: env[`${p}SMTP_USER`],
pass: env[`${p}SMTP_PASS`],
secure: env[`${p}SMTP_SECURE`] === 'true',
from: env[`${p}SMTP_FROM`] || env[`${p}SMTP_USER`],
rejectUnauthorized: env[`${p}SMTP_REJECT_UNAUTHORIZED`] !== 'false',
},
allowedReadDirs: (env.ALLOWED_READ_DIRS || '').split(',').map(d => d.trim()).filter(Boolean),
allowedWriteDirs: (env.ALLOWED_WRITE_DIRS || '').split(',').map(d => d.trim()).filter(Boolean),
};
}
// List all configured accounts from .env file
// Returns { accounts: Array, configPath: String|null }
function listAccounts() {
const envPath = findEnvPath();
if (!envPath) {
return { accounts: [], configPath: null };
}
// Parse the env file fresh to get all account prefixes
const dotenvResult = dotenv.config({ path: envPath });
const env = dotenvResult.parsed || {};
const accounts = [];
const seen = new Set();
// Check for default account (no prefix)
if (env.IMAP_HOST) {
accounts.push(createAccountObject(env, '', 'default'));
seen.add('default');
}
// Scan for named accounts (pattern: XXX_IMAP_HOST)
for (const key of Object.keys(env)) {
const match = key.match(/^([A-Z0-9]+)_IMAP_HOST$/);
if (match) {
const prefix = match[1];
const name = prefix.toLowerCase();
if (!seen.has(name)) {
accounts.push(createAccountObject(env, prefix + '_', name));
seen.add(name);
}
}
}
return { accounts, configPath: envPath };
}
// Create an account object from env variables
function createAccountObject(env, prefix, name) {
const p = prefix;
return {
name,
email: env[`${p}IMAP_USER`] || env[`${p}SMTP_FROM`] || '-',
imapHost: env[`${p}IMAP_HOST`] || '-',
smtpHost: env[`${p}SMTP_HOST`] || '-',
isComplete: isAccountComplete(env, prefix)
};
}
// Check if an account has all required configuration
function isAccountComplete(env, prefix) {
const p = prefix;
return !!(
env[`${p}IMAP_HOST`] &&
env[`${p}IMAP_USER`] &&
env[`${p}IMAP_PASS`] &&
env[`${p}SMTP_HOST`]
);
}
// --- Module initialization ---
const envPath = findEnvPath();
if (envPath) {
dotenv.config({ path: envPath });
}
const { accountName, remainingArgs } = parseAccountFromArgv(process.argv);
const prefix = accountName ? accountName.toUpperCase() : null;
// Strip --account from process.argv so callers see command at argv[2]
process.argv = [process.argv[0], process.argv[1], ...remainingArgs];
const config = buildConfig(process.env, prefix);
module.exports = config;
module.exports.listAccounts = listAccounts;
#!/usr/bin/env node
/**
* IMAP Email CLI
* Works with any standard IMAP server (Gmail, ProtonMail Bridge, Fastmail, etc.)
* Supports IMAP ID extension (RFC 2971) for 163.com and other servers
*/
const Imap = require('imap');
const { simpleParser } = require('mailparser');
const path = require('path');
const fs = require('fs');
const os = require('os');
const config = require('./config');
function validateWritePath(dirPath) {
if (!config.allowedWriteDirs.length) {
throw new Error('ALLOWED_WRITE_DIRS not set in .env. Attachment download is disabled.');
}
const resolved = path.resolve(dirPath.replace(/^~/, os.homedir()));
const allowedDirs = config.allowedWriteDirs.map(d =>
path.resolve(d.replace(/^~/, os.homedir()))
);
const allowed = allowedDirs.some(dir =>
resolved === dir || resolved.startsWith(dir + path.sep)
);
if (!allowed) {
throw new Error(`Access denied: '${dirPath}' is outside allowed write directories`);
}
return resolved;
}
function sanitizeFilename(filename) {
return path.basename(filename).replace(/\.\./g, '').replace(/^[./\\]/, '') || 'attachment';
}
// IMAP ID information for 163.com compatibility
const IMAP_ID = {
name: 'openclaw',
version: '0.0.1',
vendor: 'netease',
'support-email': 'kefu@188.com'
};
const DEFAULT_MAILBOX = config.imap.mailbox;
// Parse command-line arguments
function parseArgs() {
const args = process.argv.slice(2);
const command = args[0];
const options = {};
const positional = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
options[key] = value || true;
if (value && !value.startsWith('--')) i++;
} else {
positional.push(arg);
}
}
return { command, options, positional };
}
// Create IMAP connection config
function createImapConfig() {
return {
user: config.imap.user,
password: config.imap.pass,
host: config.imap.host,
port: config.imap.port,
tls: config.imap.tls,
tlsOptions: {
rejectUnauthorized: config.imap.rejectUnauthorized,
},
connTimeout: 10000,
authTimeout: 10000,
};
}
// Connect to IMAP server with ID support
async function connect() {
const imapConfig = createImapConfig();
if (!imapConfig.user || !imapConfig.password) {
throw new Error('Missing IMAP user or password. Check your config at ~/.config/imap-smtp-email/.env');
}
return new Promise((resolve, reject) => {
const imap = new Imap(imapConfig);
imap.once('ready', () => {
// Send IMAP ID command for 163.com compatibility
if (typeof imap.id === 'function') {
imap.id(IMAP_ID, (err) => {
if (err) {
console.warn('Warning: IMAP ID command failed:', err.message);
}
resolve(imap);
});
} else {
// ID not supported, continue without it
resolve(imap);
}
});
imap.once('error', (err) => {
reject(new Error(`IMAP connection failed: ${err.message}`));
});
imap.connect();
});
}
// Open mailbox and return promise
function openBox(imap, mailbox, readOnly = false) {
return new Promise((resolve, reject) => {
imap.openBox(mailbox, readOnly, (err, box) => {
if (err) reject(err);
else resolve(box);
});
});
}
// Search for messages
function searchMessages(imap, criteria, fetchOptions) {
return new Promise((resolve, reject) => {
imap.search(criteria, (err, results) => {
if (err) {
reject(err);
return;
}
if (!results || results.length === 0) {
resolve([]);
return;
}
const fetch = imap.fetch(results, fetchOptions);
const messages = [];
fetch.on('message', (msg) => {
const parts = [];
msg.on('body', (stream, info) => {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
stream.once('end', () => {
parts.push({ which: info.which, body: buffer });
});
});
msg.once('attributes', (attrs) => {
parts.forEach((part) => {
part.attributes = attrs;
});
});
msg.once('end', () => {
if (parts.length > 0) {
messages.push(parts[0]);
}
});
});
fetch.once('error', (err) => {
reject(err);
});
fetch.once('end', () => {
resolve(messages);
});
});
});
}
// Parse email from raw buffer
async function parseEmail(bodyStr, includeAttachments = false) {
const parsed = await simpleParser(bodyStr);
return {
from: parsed.from?.text || 'Unknown',
to: parsed.to?.text,
subject: parsed.subject || '(no subject)',
date: parsed.date,
text: parsed.text,
html: parsed.html,
snippet: parsed.text
? parsed.text.slice(0, 200)
: (parsed.html ? parsed.html.slice(0, 200).replace(/<[^>]*>/g, '') : ''),
attachments: parsed.attachments?.map((a) => ({
filename: a.filename,
contentType: a.contentType,
size: a.size,
content: includeAttachments ? a.content : undefined,
cid: a.cid,
})),
};
}
// Check for new/unread emails
async function checkEmails(mailbox = DEFAULT_MAILBOX, limit = 10, recentTime = null, unreadOnly = false) {
const imap = await connect();
try {
await openBox(imap, mailbox);
// Build search criteria
const searchCriteria = unreadOnly ? ['UNSEEN'] : ['ALL'];
if (recentTime) {
const sinceDate = parseRelativeTime(recentTime);
searchCriteria.push(['SINCE', sinceDate]);
}
// Fetch messages sorted by date (newest first)
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
// Sort by date (newest first) - parse from message attributes
const sortedMessages = messages.sort((a, b) => {
const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
return dateB - dateA;
}).slice(0, limit);
const results = [];
for (const item of sortedMessages) {
const bodyStr = item.body;
const parsed = await parseEmail(bodyStr);
results.push({
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
});
}
return results;
} finally {
imap.end();
}
}
// Fetch full email by UID
async function fetchEmail(uid, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
const searchCriteria = [['UID', uid]];
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
if (messages.length === 0) {
throw new Error(`Message UID ${uid} not found`);
}
const item = messages[0];
const parsed = await parseEmail(item.body);
return {
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
};
} finally {
imap.end();
}
}
// Download attachments from email
async function downloadAttachments(uid, mailbox = DEFAULT_MAILBOX, outputDir = '.', specificFilename = null) {
const imap = await connect();
try {
await openBox(imap, mailbox);
const searchCriteria = [['UID', uid]];
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, searchCriteria, fetchOptions);
if (messages.length === 0) {
throw new Error(`Message UID ${uid} not found`);
}
const item = messages[0];
const parsed = await parseEmail(item.body, true);
if (!parsed.attachments || parsed.attachments.length === 0) {
return {
uid,
downloaded: [],
message: 'No attachments found',
};
}
// Create output directory if it doesn't exist
const resolvedDir = validateWritePath(outputDir);
if (!fs.existsSync(resolvedDir)) {
fs.mkdirSync(resolvedDir, { recursive: true });
}
const downloaded = [];
for (const attachment of parsed.attachments) {
// If specificFilename is provided, only download matching attachment
if (specificFilename && attachment.filename !== specificFilename) {
continue;
}
if (attachment.content) {
const filePath = path.join(resolvedDir, sanitizeFilename(attachment.filename));
fs.writeFileSync(filePath, attachment.content);
downloaded.push({
filename: attachment.filename,
path: filePath,
size: attachment.size,
});
}
}
// If specific file was requested but not found
if (specificFilename && downloaded.length === 0) {
const availableFiles = parsed.attachments.map(a => a.filename).join(', ');
return {
uid,
downloaded: [],
message: `File "${specificFilename}" not found. Available attachments: ${availableFiles}`,
};
}
return {
uid,
downloaded,
message: `Downloaded ${downloaded.length} attachment(s)`,
};
} finally {
imap.end();
}
}
// Parse relative time (e.g., "2h", "30m", "7d") to Date
function parseRelativeTime(timeStr) {
const match = timeStr.match(/^(\d+)(m|h|d)$/);
if (!match) {
throw new Error('Invalid time format. Use: 30m, 2h, 7d');
}
const value = parseInt(match[1]);
const unit = match[2];
const now = new Date();
switch (unit) {
case 'm': // minutes
return new Date(now.getTime() - value * 60 * 1000);
case 'h': // hours
return new Date(now.getTime() - value * 60 * 60 * 1000);
case 'd': // days
return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
default:
throw new Error('Unknown time unit');
}
}
// Search emails with criteria
async function searchEmails(options) {
const imap = await connect();
try {
const mailbox = options.mailbox || DEFAULT_MAILBOX;
await openBox(imap, mailbox);
const criteria = [];
if (options.unseen) criteria.push('UNSEEN');
if (options.seen) criteria.push('SEEN');
if (options.from) criteria.push(['FROM', options.from]);
if (options.subject) criteria.push(['SUBJECT', options.subject]);
// Handle relative time (--recent 2h)
if (options.recent) {
const sinceDate = parseRelativeTime(options.recent);
criteria.push(['SINCE', sinceDate]);
} else {
// Handle absolute dates
if (options.since) criteria.push(['SINCE', options.since]);
if (options.before) criteria.push(['BEFORE', options.before]);
}
// Default to all if no criteria
if (criteria.length === 0) criteria.push('ALL');
const fetchOptions = {
bodies: [''],
markSeen: false,
};
const messages = await searchMessages(imap, criteria, fetchOptions);
const limit = parseInt(options.limit) || 20;
const results = [];
// Sort by date (newest first)
const sortedMessages = messages.sort((a, b) => {
const dateA = a.attributes.date ? new Date(a.attributes.date) : new Date(0);
const dateB = b.attributes.date ? new Date(b.attributes.date) : new Date(0);
return dateB - dateA;
}).slice(0, limit);
for (const item of sortedMessages) {
const parsed = await parseEmail(item.body);
results.push({
uid: item.attributes.uid,
...parsed,
flags: item.attributes.flags,
});
}
return results;
} finally {
imap.end();
}
}
// Mark message(s) as read
async function markAsRead(uids, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
return new Promise((resolve, reject) => {
imap.addFlags(uids, '\\Seen', (err) => {
if (err) reject(err);
else resolve({ success: true, uids, action: 'marked as read' });
});
});
} finally {
imap.end();
}
}
// Mark message(s) as unread
async function markAsUnread(uids, mailbox = DEFAULT_MAILBOX) {
const imap = await connect();
try {
await openBox(imap, mailbox);
return new Promise((resolve, reject) => {
imap.delFlags(uids, '\\Seen', (err) => {
if (err) reject(err);
else resolve({ success: true, uids, action: 'marked as unread' });
});
});
} finally {
imap.end();
}
}
// List all mailboxes
async function listMailboxes() {
const imap = await connect();
try {
return new Promise((resolve, reject) => {
imap.getBoxes((err, boxes) => {
if (err) reject(err);
else resolve(formatMailboxTree(boxes));
});
});
} finally {
imap.end();
}
}
// Format mailbox tree recursively
function formatMailboxTree(boxes, prefix = '') {
const result = [];
for (const [name, info] of Object.entries(boxes)) {
const fullName = prefix ? `${prefix}${info.delimiter}${name}` : name;
result.push({
name: fullName,
delimiter: info.delimiter,
attributes: info.attribs,
});
if (info.children) {
result.push(...formatMailboxTree(info.children, fullName));
}
}
return result;
}
// Display accounts in a formatted table
function displayAccounts(accounts, configPath) {
// Handle no config file case
if (!configPath) {
console.error('No configuration file found.');
console.error('Run "bash setup.sh" to configure your email account.');
process.exit(1);
}
// Handle no accounts case
if (accounts.length === 0) {
console.error(`No accounts configured in ${configPath}`);
process.exit(0);
}
// Display header with config path
console.log(`Configured accounts (from ${configPath}):\n`);
// Calculate column widths
const maxNameLen = Math.max(7, ...accounts.map(a => a.name.length)); // 7 = 'Account'.length
const maxEmailLen = Math.max(5, ...accounts.map(a => a.email.length)); // 5 = 'Email'.length
const maxImapLen = Math.max(4, ...accounts.map(a => a.imapHost.length)); // 4 = 'IMAP'.length
const maxSmtpLen = Math.max(4, ...accounts.map(a => a.smtpHost.length)); // 4 = 'SMTP'.length
// Table header
const header = ` ${padRight('Account', maxNameLen)} ${padRight('Email', maxEmailLen)} ${padRight('IMAP', maxImapLen)} ${padRight('SMTP', maxSmtpLen)} Status`;
console.log(header);
// Separator line
const separator = ' ' + '─'.repeat(maxNameLen) + ' ' + '─'.repeat(maxEmailLen) + ' ' + '─'.repeat(maxImapLen) + ' ' + '─'.repeat(maxSmtpLen) + ' ' + '────────────────';
console.log(separator);
// Table rows
for (const account of accounts) {
const statusIcon = account.isComplete ? '✓' : '⚠';
const statusText = account.isComplete ? 'Complete' : 'Incomplete';
const row = ` ${padRight(account.name, maxNameLen)} ${padRight(account.email, maxEmailLen)} ${padRight(account.imapHost, maxImapLen)} ${padRight(account.smtpHost, maxSmtpLen)} ${statusIcon} ${statusText}`;
console.log(row);
}
// Footer
console.log(`\n ${accounts.length} account${accounts.length > 1 ? 's' : ''} total`);
}
// Helper: right-pad a string to a fixed width
function padRight(str, len) {
return (str + ' '.repeat(len)).slice(0, len);
}
// Main CLI handler
async function main() {
const { command, options, positional } = parseArgs();
try {
let result;
switch (command) {
case 'check':
result = await checkEmails(
options.mailbox || DEFAULT_MAILBOX,
parseInt(options.limit) || 10,
options.recent || null,
options.unseen === 'true' // if --unseen is set, only get unread messages
);
break;
case 'fetch':
if (!positional[0]) {
throw new Error('UID required: node imap.js fetch <uid>');
}
result = await fetchEmail(positional[0], options.mailbox);
break;
case 'download':
if (!positional[0]) {
throw new Error('UID required: node imap.js download <uid>');
}
result = await downloadAttachments(positional[0], options.mailbox, options.dir || '.', options.file || null);
break;
case 'search':
result = await searchEmails(options);
break;
case 'mark-read':
if (positional.length === 0) {
throw new Error('UID(s) required: node imap.js mark-read <uid> [uid2...]');
}
result = await markAsRead(positional, options.mailbox);
break;
case 'mark-unread':
if (positional.length === 0) {
throw new Error('UID(s) required: node imap.js mark-unread <uid> [uid2...]');
}
result = await markAsUnread(positional, options.mailbox);
break;
case 'list-mailboxes':
result = await listMailboxes();
break;
case 'list-accounts':
{
const { listAccounts } = require('./config');
const { accounts, configPath } = listAccounts();
displayAccounts(accounts, configPath);
}
return; // Exit early, no JSON output
default:
console.error('Unknown command:', command);
console.error('Available commands: check, fetch, download, search, mark-read, mark-unread, list-mailboxes, list-accounts');
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();
#!/usr/bin/env node
/**
* SMTP Email CLI
* Send email via SMTP protocol. Works with Gmail, Outlook, 163.com, and any standard SMTP server.
* Supports attachments, HTML content, and multiple recipients.
*/
const nodemailer = require('nodemailer');
const path = require('path');
const os = require('os');
const fs = require('fs');
const config = require('./config');
function validateReadPath(inputPath) {
let realPath;
try {
realPath = fs.realpathSync(inputPath);
} catch {
realPath = path.resolve(inputPath);
}
if (!config.allowedReadDirs.length) {
throw new Error('ALLOWED_READ_DIRS not set in .env. File read operations are disabled.');
}
const allowedDirs = config.allowedReadDirs.map(d =>
path.resolve(d.replace(/^~/, os.homedir()))
);
const allowed = allowedDirs.some(dir =>
realPath === dir || realPath.startsWith(dir + path.sep)
);
if (!allowed) {
throw new Error(`Access denied: '${inputPath}' is outside allowed read directories`);
}
return realPath;
}
// Parse command-line arguments
function parseArgs() {
const args = process.argv.slice(2);
const command = args[0];
const options = {};
const positional = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
options[key] = value || true;
if (value && !value.startsWith('--')) i++;
} else {
positional.push(arg);
}
}
return { command, options, positional };
}
// Create SMTP transporter
function createTransporter() {
if (!config.smtp.host || !config.smtp.user || !config.smtp.pass) {
throw new Error('Missing SMTP configuration. Check your config at ~/.config/imap-smtp-email/.env');
}
return nodemailer.createTransport({
host: config.smtp.host,
port: config.smtp.port,
secure: config.smtp.secure,
auth: {
user: config.smtp.user,
pass: config.smtp.pass,
},
tls: {
rejectUnauthorized: config.smtp.rejectUnauthorized,
},
});
}
// Send email
async function sendEmail(options) {
const transporter = createTransporter();
// Verify connection
try {
await transporter.verify();
console.error('SMTP server is ready to send');
} catch (err) {
throw new Error(`SMTP connection failed: ${err.message}`);
}
const mailOptions = {
from: options.from || config.smtp.from,
to: options.to,
cc: options.cc || undefined,
bcc: options.bcc || undefined,
subject: options.subject || '(no subject)',
text: options.text || undefined,
html: options.html || undefined,
attachments: options.attachments || [],
};
// If neither text nor html provided, use default text
if (!mailOptions.text && !mailOptions.html) {
mailOptions.text = options.body || '';
}
const info = await transporter.sendMail(mailOptions);
return {
success: true,
messageId: info.messageId,
response: info.response,
to: mailOptions.to,
};
}
// Read file content for attachments
function readAttachment(filePath) {
validateReadPath(filePath);
if (!fs.existsSync(filePath)) {
throw new Error(`Attachment file not found: ${filePath}`);
}
return {
filename: path.basename(filePath),
path: path.resolve(filePath),
};
}
// Send email with file content
async function sendEmailWithContent(options) {
// Handle attachments
if (options.attach) {
const attachFiles = options.attach.split(',').map(f => f.trim());
options.attachments = attachFiles.map(f => readAttachment(f));
}
return await sendEmail(options);
}
// Test SMTP connection
async function testConnection() {
const transporter = createTransporter();
try {
await transporter.verify();
const info = await transporter.sendMail({
from: config.smtp.from || config.smtp.user,
to: config.smtp.user,
subject: 'SMTP Connection Test',
text: 'This is a test email from the IMAP/SMTP email skill.',
html: '<p>This is a <strong>test email</strong> from the IMAP/SMTP email skill.</p>',
});
return {
success: true,
message: 'SMTP connection successful',
messageId: info.messageId,
};
} catch (err) {
throw new Error(`SMTP test failed: ${err.message}`);
}
}
// Display accounts in a formatted table
function displayAccounts(accounts, configPath) {
// Handle no config file case
if (!configPath) {
console.error('No configuration file found.');
console.error('Run "bash setup.sh" to configure your email account.');
process.exit(1);
}
// Handle no accounts case
if (accounts.length === 0) {
console.error(`No accounts configured in ${configPath}`);
process.exit(0);
}
// Display header with config path
console.log(`Configured accounts (from ${configPath}):\n`);
// Calculate column widths
const maxNameLen = Math.max(7, ...accounts.map(a => a.name.length)); // 7 = 'Account'.length
const maxEmailLen = Math.max(5, ...accounts.map(a => a.email.length)); // 5 = 'Email'.length
const maxImapLen = Math.max(4, ...accounts.map(a => a.imapHost.length)); // 4 = 'IMAP'.length
const maxSmtpLen = Math.max(4, ...accounts.map(a => a.smtpHost.length)); // 4 = 'SMTP'.length
// Table header
const header = ` ${padRight('Account', maxNameLen)} ${padRight('Email', maxEmailLen)} ${padRight('IMAP', maxImapLen)} ${padRight('SMTP', maxSmtpLen)} Status`;
console.log(header);
// Separator line
const separator = ' ' + '─'.repeat(maxNameLen) + ' ' + '─'.repeat(maxEmailLen) + ' ' + '─'.repeat(maxImapLen) + ' ' + '─'.repeat(maxSmtpLen) + ' ' + '────────────────';
console.log(separator);
// Table rows
for (const account of accounts) {
const statusIcon = account.isComplete ? '✓' : '⚠';
const statusText = account.isComplete ? 'Complete' : 'Incomplete';
const row = ` ${padRight(account.name, maxNameLen)} ${padRight(account.email, maxEmailLen)} ${padRight(account.imapHost, maxImapLen)} ${padRight(account.smtpHost, maxSmtpLen)} ${statusIcon} ${statusText}`;
console.log(row);
}
// Footer
console.log(`\n ${accounts.length} account${accounts.length > 1 ? 's' : ''} total`);
}
// Helper: right-pad a string to a fixed width
function padRight(str, len) {
return (str + ' '.repeat(len)).slice(0, len);
}
// Main CLI handler
async function main() {
const { command, options, positional } = parseArgs();
try {
let result;
switch (command) {
case 'send':
if (!options.to) {
throw new Error('Missing required option: --to <email>');
}
if (!options.subject && !options['subject-file']) {
throw new Error('Missing required option: --subject <text> or --subject-file <file>');
}
// Read subject from file if specified
if (options['subject-file']) {
validateReadPath(options['subject-file']);
options.subject = fs.readFileSync(options['subject-file'], 'utf8').trim();
}
// Read body from file if specified
if (options['body-file']) {
validateReadPath(options['body-file']);
const content = fs.readFileSync(options['body-file'], 'utf8');
if (options['body-file'].endsWith('.html') || options.html) {
options.html = content;
} else {
options.text = content;
}
} else if (options['html-file']) {
validateReadPath(options['html-file']);
options.html = fs.readFileSync(options['html-file'], 'utf8');
} else if (options.body) {
options.text = options.body;
}
result = await sendEmailWithContent(options);
break;
case 'test':
result = await testConnection();
break;
case 'list-accounts':
{
const { listAccounts } = require('./config');
const { accounts, configPath } = listAccounts();
displayAccounts(accounts, configPath);
}
return; // Exit early, no JSON output
default:
console.error('Unknown command:', command);
console.error('Available commands: send, test, list-accounts');
console.error('\nUsage:');
console.error(' send --to <email> --subject <text> [--body <text>] [--html] [--cc <email>] [--bcc <email>] [--attach <file>]');
console.error(' send --to <email> --subject <text> --body-file <file> [--html-file <file>] [--attach <file>]');
console.error(' test Test SMTP connection');
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();
#!/bin/bash
# IMAP/SMTP Email Skill Setup Helper
CONFIG_DIR="$HOME/.config/imap-smtp-email"
CONFIG_FILE="$CONFIG_DIR/.env"
echo "================================"
echo " IMAP/SMTP Email Skill Setup"
echo "================================"
echo ""
# Determine setup mode
SETUP_MODE="default"
ACCOUNT_PREFIX=""
ACCOUNT_NAME=""
if [ -f "$CONFIG_FILE" ]; then
echo "Existing configuration found at $CONFIG_FILE"
echo ""
echo "What would you like to do?"
echo " 1) Reconfigure default account"
echo " 2) Add a new account"
echo ""
read -p "Enter choice (1-2): " SETUP_CHOICE
case $SETUP_CHOICE in
1)
SETUP_MODE="reconfigure"
;;
2)
SETUP_MODE="add"
while true; do
read -p "Account name (letters/digits only, e.g. work): " ACCOUNT_NAME
if [[ "$ACCOUNT_NAME" =~ ^[a-zA-Z0-9]+$ ]]; then
ACCOUNT_PREFIX="$(echo "$ACCOUNT_NAME" | tr '[:lower:]' '[:upper:]')_"
# Check if account already exists
if grep -q "^${ACCOUNT_PREFIX}IMAP_HOST=" "$CONFIG_FILE" 2>/dev/null; then
read -p "Account \"$ACCOUNT_NAME\" already exists. Overwrite? (y/n): " OVERWRITE
if [ "$OVERWRITE" != "y" ]; then
echo "Aborted."
exit 0
fi
SETUP_MODE="overwrite"
fi
break
else
echo "Invalid name. Use only letters and digits."
fi
done
;;
*)
echo "Invalid choice"
exit 1
;;
esac
fi
echo ""
echo "This script will help you configure email credentials."
echo ""
# Prompt for email provider
echo "Select your email provider:"
echo " 1) Gmail"
echo " 2) Outlook"
echo " 3) 163.com"
echo " 4) vip.163.com"
echo " 5) 126.com"
echo " 6) vip.126.com"
echo " 7) 188.com"
echo " 8) vip.188.com"
echo " 9) yeah.net"
echo " 10) QQ Mail"
echo " 11) Custom"
echo ""
read -p "Enter choice (1-11): " PROVIDER_CHOICE
case $PROVIDER_CHOICE in
1)
IMAP_HOST="imap.gmail.com"
IMAP_PORT="993"
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_SECURE="false"
IMAP_TLS="true"
echo ""
echo "⚠️ Gmail requires an App Password — your regular Google password will NOT work."
echo " 1. Go to: https://myaccount.google.com/apppasswords"
echo " 2. Generate an App Password (requires 2-Step Verification enabled)"
echo " 3. Use the generated 16-character password below"
echo ""
;;
2)
IMAP_HOST="outlook.office365.com"
IMAP_PORT="993"
SMTP_HOST="smtp.office365.com"
SMTP_PORT="587"
SMTP_SECURE="false"
IMAP_TLS="true"
;;
3)
IMAP_HOST="imap.163.com"
IMAP_PORT="993"
SMTP_HOST="smtp.163.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
4)
IMAP_HOST="imap.vip.163.com"
IMAP_PORT="993"
SMTP_HOST="smtp.vip.163.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
5)
IMAP_HOST="imap.126.com"
IMAP_PORT="993"
SMTP_HOST="smtp.126.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
6)
IMAP_HOST="imap.vip.126.com"
IMAP_PORT="993"
SMTP_HOST="smtp.vip.126.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
7)
IMAP_HOST="imap.188.com"
IMAP_PORT="993"
SMTP_HOST="smtp.188.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
8)
IMAP_HOST="imap.vip.188.com"
IMAP_PORT="993"
SMTP_HOST="smtp.vip.188.com"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
9)
IMAP_HOST="imap.yeah.net"
IMAP_PORT="993"
SMTP_HOST="smtp.yeah.net"
SMTP_PORT="465"
SMTP_SECURE="true"
IMAP_TLS="true"
;;
10)
IMAP_HOST="imap.qq.com"
IMAP_PORT="993"
SMTP_HOST="smtp.qq.com"
SMTP_PORT="587"
SMTP_SECURE="false"
IMAP_TLS="true"
;;
11)
read -p "IMAP Host: " IMAP_HOST
read -p "IMAP Port: " IMAP_PORT
read -p "SMTP Host: " SMTP_HOST
read -p "SMTP Port: " SMTP_PORT
read -p "Use TLS for IMAP? (true/false): " IMAP_TLS
read -p "Use SSL for SMTP? (true/false): " SMTP_SECURE
;;
*)
echo "Invalid choice"
exit 1
;;
esac
echo ""
read -p "Email address: " EMAIL
read -s -p "Password / App Password / Authorization Code: " PASSWORD
echo ""
read -p "Accept self-signed certificates? (y/n): " ACCEPT_CERT
if [ "$ACCEPT_CERT" = "y" ]; then
REJECT_UNAUTHORIZED="false"
else
REJECT_UNAUTHORIZED="true"
fi
# Only ask for shared settings on first-time or reconfigure
ASK_SHARED=false
if [ "$SETUP_MODE" = "default" ] || [ "$SETUP_MODE" = "reconfigure" ]; then
ASK_SHARED=true
fi
if [ "$ASK_SHARED" = true ]; then
read -p "Allowed directories for reading files (comma-separated, e.g. ~/Downloads,~/Documents): " ALLOWED_READ_DIRS
read -p "Allowed directories for saving attachments (comma-separated, e.g. ~/Downloads): " ALLOWED_WRITE_DIRS
fi
# Create config directory
mkdir -p -m 700 "$CONFIG_DIR"
# Build account variables block
ACCOUNT_VARS="# ${ACCOUNT_NAME:-Default} account
${ACCOUNT_PREFIX}IMAP_HOST=$IMAP_HOST
${ACCOUNT_PREFIX}IMAP_PORT=$IMAP_PORT
${ACCOUNT_PREFIX}IMAP_USER=$EMAIL
${ACCOUNT_PREFIX}IMAP_PASS=$PASSWORD
${ACCOUNT_PREFIX}IMAP_TLS=$IMAP_TLS
${ACCOUNT_PREFIX}IMAP_REJECT_UNAUTHORIZED=$REJECT_UNAUTHORIZED
${ACCOUNT_PREFIX}IMAP_MAILBOX=INBOX
${ACCOUNT_PREFIX}SMTP_HOST=$SMTP_HOST
${ACCOUNT_PREFIX}SMTP_PORT=$SMTP_PORT
${ACCOUNT_PREFIX}SMTP_SECURE=$SMTP_SECURE
${ACCOUNT_PREFIX}SMTP_USER=$EMAIL
${ACCOUNT_PREFIX}SMTP_PASS=$PASSWORD
${ACCOUNT_PREFIX}SMTP_FROM=$EMAIL
${ACCOUNT_PREFIX}SMTP_REJECT_UNAUTHORIZED=$REJECT_UNAUTHORIZED"
case $SETUP_MODE in
"default")
# First-time setup: write entire file
cat > "$CONFIG_FILE" << EOF
$ACCOUNT_VARS
# File access whitelist (security)
ALLOWED_READ_DIRS=${ALLOWED_READ_DIRS:-$HOME/Downloads,$HOME/Documents}
ALLOWED_WRITE_DIRS=${ALLOWED_WRITE_DIRS:-$HOME/Downloads}
EOF
;;
"reconfigure")
# Keep only named-account lines (pattern: NAME_IMAP_* or NAME_SMTP_*)
TEMP_FILE=$(mktemp)
grep -E '^[A-Z0-9]+_(IMAP_|SMTP_)' "$CONFIG_FILE" > "$TEMP_FILE.named" 2>/dev/null || true
cat > "$TEMP_FILE" << EOF
$ACCOUNT_VARS
# File access whitelist (security)
ALLOWED_READ_DIRS=${ALLOWED_READ_DIRS:-$HOME/Downloads,$HOME/Documents}
ALLOWED_WRITE_DIRS=${ALLOWED_WRITE_DIRS:-$HOME/Downloads}
EOF
# Append retained named-account lines if any
if [ -s "$TEMP_FILE.named" ]; then
echo "" >> "$TEMP_FILE"
echo "# Named accounts" >> "$TEMP_FILE"
cat "$TEMP_FILE.named" >> "$TEMP_FILE"
fi
mv "$TEMP_FILE" "$CONFIG_FILE"
rm -f "$TEMP_FILE.named"
;;
"add")
# Append named account to existing file
echo "" >> "$CONFIG_FILE"
echo "$ACCOUNT_VARS" >> "$CONFIG_FILE"
;;
"overwrite")
# Strip existing lines with this account prefix, then append new ones
TEMP_FILE=$(mktemp)
grep -v "^${ACCOUNT_PREFIX}\(IMAP_\|SMTP_\)" "$CONFIG_FILE" | grep -vi "^# ${ACCOUNT_NAME} account" > "$TEMP_FILE" 2>/dev/null || true
# Remove trailing blank lines (portable: command substitution strips trailing newlines)
content=$(cat "$TEMP_FILE") && printf '%s\n' "$content" > "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
echo "$ACCOUNT_VARS" >> "$TEMP_FILE"
mv "$TEMP_FILE" "$CONFIG_FILE"
;;
esac
echo ""
echo "✅ Configuration saved to $CONFIG_FILE"
chmod 600 "$CONFIG_FILE"
echo "✅ Set file permissions to 600 (owner read/write only)"
echo ""
echo "Testing connections..."
echo ""
# Build test command with account flag if applicable
ACCOUNT_FLAG=""
if [ -n "$ACCOUNT_NAME" ]; then
ACCOUNT_FLAG="--account $ACCOUNT_NAME"
fi
# Test IMAP connection
echo "Testing IMAP..."
if node scripts/imap.js $ACCOUNT_FLAG list-mailboxes >/dev/null 2>&1; then
echo "✅ IMAP connection successful!"
else
echo "❌ IMAP connection test failed"
echo " Please check your credentials and settings"
fi
# Test SMTP connection
echo ""
echo "Testing SMTP..."
echo " (This will send a test email to your own address: $EMAIL)"
if node scripts/smtp.js $ACCOUNT_FLAG test >/dev/null 2>&1; then
echo "✅ SMTP connection successful!"
else
echo "❌ SMTP connection test failed"
echo " Please check your credentials and settings"
fi
echo ""
echo "Setup complete! Try:"
if [ -n "$ACCOUNT_NAME" ]; then
echo " node scripts/imap.js --account $ACCOUNT_NAME check"
echo " node scripts/smtp.js --account $ACCOUNT_NAME send --to recipient@example.com --subject Test --body 'Hello World'"
else
echo " node scripts/imap.js check"
echo " node scripts/smtp.js send --to recipient@example.com --subject Test --body 'Hello World'"
fi
Related skills
FAQ
Does it work with Gmail?
Yes, but Gmail requires a 16-character App Password and 2-Step Verification, not your regular password.
Can it handle more than one account?
Yes, additional accounts use an uppercase name prefix on all config variables.