Initial backup 2026-02-17
This commit is contained in:
356
skills/imap-smtp-email/scripts/imap-py.py
Normal file
356
skills/imap-smtp-email/scripts/imap-py.py
Normal file
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IMAP Email CLI - Python version
|
||||
Supports: check, fetch, search, mark-read, mark-unread, list-mailboxes
|
||||
"""
|
||||
import imaplib
|
||||
import email
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from email.header import decode_header
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
||||
# Load .env
|
||||
env_file = Path(__file__).parent.parent / '.env'
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text().splitlines():
|
||||
if line.strip() and not line.startswith('#') and '=' in line:
|
||||
key, _, value = line.partition('=')
|
||||
os.environ[key.strip()] = value.strip()
|
||||
|
||||
def connect_imap():
|
||||
"""Connect and login to IMAP server"""
|
||||
host = os.environ.get('IMAP_HOST', 'imap.gmail.com')
|
||||
port = int(os.environ.get('IMAP_PORT', '993'))
|
||||
user = os.environ.get('IMAP_USER')
|
||||
password = os.environ.get('IMAP_PASS')
|
||||
|
||||
if not user or not password:
|
||||
print("Error: IMAP_USER and IMAP_PASS must be set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mail = imaplib.IMAP4_SSL(host, port)
|
||||
mail.login(user, password)
|
||||
return mail
|
||||
|
||||
def decode_header_value(value):
|
||||
"""Decode email header value"""
|
||||
if not value:
|
||||
return ""
|
||||
decoded_parts = decode_header(value)
|
||||
result = []
|
||||
for part, encoding in decoded_parts:
|
||||
if isinstance(part, bytes):
|
||||
result.append(part.decode(encoding or 'utf-8', errors='ignore'))
|
||||
else:
|
||||
result.append(part)
|
||||
return ' '.join(result)
|
||||
|
||||
def parse_time_filter(time_str):
|
||||
"""Parse time filter like '30m', '2h', '7d' into datetime"""
|
||||
match = re.match(r'(\d+)([mhd])', time_str)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
value, unit = int(match.group(1)), match.group(2)
|
||||
if unit == 'm':
|
||||
delta = timedelta(minutes=value)
|
||||
elif unit == 'h':
|
||||
delta = timedelta(hours=value)
|
||||
elif unit == 'd':
|
||||
delta = timedelta(days=value)
|
||||
else:
|
||||
return None
|
||||
|
||||
return datetime.now() - delta
|
||||
|
||||
def cmd_check(args):
|
||||
"""Check for recent emails"""
|
||||
mail = connect_imap()
|
||||
mailbox = args.mailbox or 'INBOX'
|
||||
mail.select(mailbox)
|
||||
|
||||
# Search criteria
|
||||
criteria = ['ALL']
|
||||
if args.recent:
|
||||
since_date = parse_time_filter(args.recent)
|
||||
if since_date:
|
||||
date_str = since_date.strftime('%d-%b-%Y')
|
||||
criteria = [f'SINCE {date_str}']
|
||||
|
||||
# Search
|
||||
status, messages = mail.search(None, *criteria)
|
||||
if status != 'OK':
|
||||
print("Error searching mailbox", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
msg_nums = messages[0].split()
|
||||
if not msg_nums:
|
||||
print("No messages found")
|
||||
mail.close()
|
||||
mail.logout()
|
||||
return
|
||||
|
||||
# Get last N messages
|
||||
limit = int(args.limit) if args.limit else 10
|
||||
msg_nums = msg_nums[-limit:]
|
||||
|
||||
print(f"Showing {len(msg_nums)} most recent messages:\n")
|
||||
|
||||
for num in reversed(msg_nums):
|
||||
typ, data = mail.fetch(num, '(FLAGS BODY[HEADER.FIELDS (FROM SUBJECT DATE)])')
|
||||
if data[0]:
|
||||
msg_data = data[0][1].decode('utf-8', errors='ignore')
|
||||
msg = email.message_from_string(msg_data)
|
||||
|
||||
from_addr = decode_header_value(msg.get('From', ''))
|
||||
subject = decode_header_value(msg.get('Subject', ''))
|
||||
date = msg.get('Date', '')
|
||||
|
||||
# Check if unread
|
||||
flags = data[1].decode('utf-8', errors='ignore') if len(data) > 1 else ''
|
||||
unread = '●' if '\\Seen' not in flags else ' '
|
||||
|
||||
print(f"{unread} UID: {num.decode()}")
|
||||
print(f" From: {from_addr}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Date: {date}")
|
||||
print()
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
|
||||
def cmd_fetch(args):
|
||||
"""Fetch full email content"""
|
||||
if not args.uid:
|
||||
print("Error: UID required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mail = connect_imap()
|
||||
mailbox = args.mailbox or 'INBOX'
|
||||
mail.select(mailbox)
|
||||
|
||||
uid = args.uid
|
||||
typ, data = mail.fetch(uid, '(RFC822)')
|
||||
|
||||
if data[0] is None:
|
||||
print(f"Error: Message {uid} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
msg = email.message_from_bytes(data[0][1])
|
||||
|
||||
print(f"From: {decode_header_value(msg.get('From', ''))}")
|
||||
print(f"To: {decode_header_value(msg.get('To', ''))}")
|
||||
print(f"Subject: {decode_header_value(msg.get('Subject', ''))}")
|
||||
print(f"Date: {msg.get('Date', '')}")
|
||||
print("-" * 80)
|
||||
|
||||
# Get body
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
if content_type == 'text/plain':
|
||||
body = part.get_payload(decode=True)
|
||||
if body:
|
||||
print(body.decode('utf-8', errors='ignore'))
|
||||
break
|
||||
elif content_type == 'text/html' and args.html:
|
||||
body = part.get_payload(decode=True)
|
||||
if body:
|
||||
print(body.decode('utf-8', errors='ignore'))
|
||||
break
|
||||
else:
|
||||
body = msg.get_payload(decode=True)
|
||||
if body:
|
||||
print(body.decode('utf-8', errors='ignore'))
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
|
||||
def cmd_search(args):
|
||||
"""Search emails with filters"""
|
||||
mail = connect_imap()
|
||||
mailbox = args.mailbox or 'INBOX'
|
||||
mail.select(mailbox)
|
||||
|
||||
# Build search criteria
|
||||
criteria = []
|
||||
|
||||
if args.unseen:
|
||||
criteria.append('UNSEEN')
|
||||
if args.seen:
|
||||
criteria.append('SEEN')
|
||||
if args.from_:
|
||||
criteria.extend(['FROM', f'"{args.from_}"'])
|
||||
if args.subject:
|
||||
criteria.extend(['SUBJECT', f'"{args.subject}"'])
|
||||
if args.since:
|
||||
criteria.extend(['SINCE', args.since])
|
||||
if args.before:
|
||||
criteria.extend(['BEFORE', args.before])
|
||||
if args.recent:
|
||||
since_date = parse_time_filter(args.recent)
|
||||
if since_date:
|
||||
date_str = since_date.strftime('%d-%b-%Y')
|
||||
criteria.extend(['SINCE', date_str])
|
||||
|
||||
if not criteria:
|
||||
criteria = ['ALL']
|
||||
|
||||
# Search
|
||||
status, messages = mail.search(None, *criteria)
|
||||
if status != 'OK':
|
||||
print("Error searching mailbox", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
msg_nums = messages[0].split()
|
||||
if not msg_nums:
|
||||
print("No messages found")
|
||||
mail.close()
|
||||
mail.logout()
|
||||
return
|
||||
|
||||
# Get last N messages
|
||||
limit = int(args.limit) if args.limit else 20
|
||||
msg_nums = msg_nums[-limit:]
|
||||
|
||||
print(f"Found {len(msg_nums)} messages:\n")
|
||||
|
||||
for num in reversed(msg_nums):
|
||||
typ, data = mail.fetch(num, '(FLAGS BODY[HEADER.FIELDS (FROM SUBJECT DATE)])')
|
||||
if data[0]:
|
||||
msg_data = data[0][1].decode('utf-8', errors='ignore')
|
||||
msg = email.message_from_string(msg_data)
|
||||
|
||||
from_addr = decode_header_value(msg.get('From', ''))
|
||||
subject = decode_header_value(msg.get('Subject', ''))
|
||||
date = msg.get('Date', '')
|
||||
|
||||
flags = data[1].decode('utf-8', errors='ignore') if len(data) > 1 else ''
|
||||
unread = '●' if '\\Seen' not in flags else ' '
|
||||
|
||||
print(f"{unread} UID: {num.decode()}")
|
||||
print(f" From: {from_addr}")
|
||||
print(f" Subject: {subject}")
|
||||
print(f" Date: {date}")
|
||||
print()
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
|
||||
def cmd_mark_read(args):
|
||||
"""Mark message(s) as read"""
|
||||
if not args.uids:
|
||||
print("Error: At least one UID required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mail = connect_imap()
|
||||
mailbox = args.mailbox or 'INBOX'
|
||||
mail.select(mailbox)
|
||||
|
||||
for uid in args.uids:
|
||||
mail.store(uid, '+FLAGS', '\\Seen')
|
||||
print(f"Marked {uid} as read")
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
|
||||
def cmd_mark_unread(args):
|
||||
"""Mark message(s) as unread"""
|
||||
if not args.uids:
|
||||
print("Error: At least one UID required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mail = connect_imap()
|
||||
mailbox = args.mailbox or 'INBOX'
|
||||
mail.select(mailbox)
|
||||
|
||||
for uid in args.uids:
|
||||
mail.store(uid, '-FLAGS', '\\Seen')
|
||||
print(f"Marked {uid} as unread")
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
|
||||
def cmd_list_mailboxes(args):
|
||||
"""List all mailboxes"""
|
||||
mail = connect_imap()
|
||||
|
||||
status, mailboxes = mail.list()
|
||||
if status == 'OK':
|
||||
print("Available mailboxes:")
|
||||
for mb in mailboxes:
|
||||
parts = mb.decode().split(' "/" ')
|
||||
if len(parts) >= 2:
|
||||
name = parts[1].strip('"')
|
||||
print(f" {name}")
|
||||
|
||||
mail.logout()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='IMAP Email CLI')
|
||||
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
|
||||
|
||||
# check
|
||||
check_parser = subparsers.add_parser('check', help='Check for new/unread emails')
|
||||
check_parser.add_argument('--limit', help='Max results', default='10')
|
||||
check_parser.add_argument('--mailbox', help='Mailbox name', default='INBOX')
|
||||
check_parser.add_argument('--recent', help='Only show emails from last X time (e.g., 30m, 2h, 7d)')
|
||||
|
||||
# fetch
|
||||
fetch_parser = subparsers.add_parser('fetch', help='Fetch full email content')
|
||||
fetch_parser.add_argument('uid', help='Email UID')
|
||||
fetch_parser.add_argument('--mailbox', help='Mailbox name', default='INBOX')
|
||||
fetch_parser.add_argument('--html', action='store_true', help='Show HTML content')
|
||||
|
||||
# search
|
||||
search_parser = subparsers.add_parser('search', help='Search emails')
|
||||
search_parser.add_argument('--unseen', action='store_true', help='Only unread messages')
|
||||
search_parser.add_argument('--seen', action='store_true', help='Only read messages')
|
||||
search_parser.add_argument('--from', dest='from_', help='From address contains')
|
||||
search_parser.add_argument('--subject', help='Subject contains')
|
||||
search_parser.add_argument('--recent', help='From last X time (e.g., 30m, 2h, 7d)')
|
||||
search_parser.add_argument('--since', help='After date (DD-Mon-YYYY)')
|
||||
search_parser.add_argument('--before', help='Before date (DD-Mon-YYYY)')
|
||||
search_parser.add_argument('--limit', help='Max results', default='20')
|
||||
search_parser.add_argument('--mailbox', help='Mailbox name', default='INBOX')
|
||||
|
||||
# mark-read
|
||||
read_parser = subparsers.add_parser('mark-read', help='Mark message(s) as read')
|
||||
read_parser.add_argument('uids', nargs='+', help='Email UIDs')
|
||||
read_parser.add_argument('--mailbox', help='Mailbox name', default='INBOX')
|
||||
|
||||
# mark-unread
|
||||
unread_parser = subparsers.add_parser('mark-unread', help='Mark message(s) as unread')
|
||||
unread_parser.add_argument('uids', nargs='+', help='Email UIDs')
|
||||
unread_parser.add_argument('--mailbox', help='Mailbox name', default='INBOX')
|
||||
|
||||
# list-mailboxes
|
||||
subparsers.add_parser('list-mailboxes', help='List all mailboxes')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Execute command
|
||||
if args.command == 'check':
|
||||
cmd_check(args)
|
||||
elif args.command == 'fetch':
|
||||
cmd_fetch(args)
|
||||
elif args.command == 'search':
|
||||
cmd_search(args)
|
||||
elif args.command == 'mark-read':
|
||||
cmd_mark_read(args)
|
||||
elif args.command == 'mark-unread':
|
||||
cmd_mark_unread(args)
|
||||
elif args.command == 'list-mailboxes':
|
||||
cmd_list_mailboxes(args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
60
skills/imap-smtp-email/scripts/imap-test.py
Normal file
60
skills/imap-smtp-email/scripts/imap-test.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick IMAP test using Python's imaplib
|
||||
"""
|
||||
import imaplib
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Load .env
|
||||
env_file = Path(__file__).parent.parent / '.env'
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text().splitlines():
|
||||
if line.strip() and not line.startswith('#'):
|
||||
key, _, value = line.partition('=')
|
||||
os.environ[key.strip()] = value.strip()
|
||||
|
||||
def test_imap():
|
||||
host = os.environ.get('IMAP_HOST', 'imap.gmail.com')
|
||||
port = int(os.environ.get('IMAP_PORT', '993'))
|
||||
user = os.environ.get('IMAP_USER')
|
||||
password = os.environ.get('IMAP_PASS')
|
||||
|
||||
print(f"Connecting to {host}:{port}...", file=sys.stderr)
|
||||
|
||||
try:
|
||||
# Connect
|
||||
mail = imaplib.IMAP4_SSL(host, port)
|
||||
print(f"Connected! Server says: {mail.welcome}", file=sys.stderr)
|
||||
|
||||
# Login
|
||||
mail.login(user, password)
|
||||
print(f"Logged in as {user}", file=sys.stderr)
|
||||
|
||||
# Select inbox
|
||||
status, messages = mail.select('INBOX')
|
||||
num_messages = int(messages[0])
|
||||
print(f"INBOX has {num_messages} messages", file=sys.stderr)
|
||||
|
||||
# Fetch last 5 messages
|
||||
if num_messages > 0:
|
||||
start = max(1, num_messages - 4)
|
||||
typ, data = mail.fetch(f'{start}:{num_messages}', '(FLAGS BODY[HEADER.FIELDS (FROM SUBJECT DATE)])')
|
||||
|
||||
for i in range(0, len(data), 2):
|
||||
if data[i]:
|
||||
msg = data[i][1].decode('utf-8', errors='ignore')
|
||||
print(msg)
|
||||
print('-' * 80)
|
||||
|
||||
mail.close()
|
||||
mail.logout()
|
||||
print("✅ IMAP test successful!", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_imap()
|
||||
559
skills/imap-smtp-email/scripts/imap.js
Normal file
559
skills/imap-smtp-email/scripts/imap.js
Normal file
@@ -0,0 +1,559 @@
|
||||
#!/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');
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// IMAP ID information for 163.com compatibility
|
||||
const IMAP_ID = {
|
||||
name: 'moltbot',
|
||||
version: '0.0.1',
|
||||
vendor: 'netease',
|
||||
'support-email': 'kefu@188.com'
|
||||
};
|
||||
|
||||
const DEFAULT_MAILBOX = process.env.IMAP_MAILBOX || 'INBOX';
|
||||
|
||||
// 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: process.env.IMAP_USER,
|
||||
password: process.env.IMAP_PASS,
|
||||
host: process.env.IMAP_HOST || '127.0.0.1',
|
||||
port: parseInt(process.env.IMAP_PORT) || 1143,
|
||||
tls: process.env.IMAP_TLS === 'true',
|
||||
tlsOptions: {
|
||||
rejectUnauthorized: process.env.IMAP_REJECT_UNAUTHORIZED !== 'false',
|
||||
},
|
||||
connTimeout: 10000,
|
||||
authTimeout: 10000,
|
||||
};
|
||||
}
|
||||
|
||||
// Connect to IMAP server with ID support
|
||||
async function connect() {
|
||||
const config = createImapConfig();
|
||||
|
||||
if (!config.user || !config.password) {
|
||||
throw new Error('Missing IMAP_USER or IMAP_PASS environment variables');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = new Imap(config);
|
||||
|
||||
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
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { 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(outputDir, 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
default:
|
||||
console.error('Unknown command:', command);
|
||||
console.error('Available commands: check, fetch, download, search, mark-read, mark-unread, list-mailboxes');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
console.error('Error:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
204
skills/imap-smtp-email/scripts/smtp.js
Normal file
204
skills/imap-smtp-email/scripts/smtp.js
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/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');
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// 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() {
|
||||
const config = {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.host || !config.auth.user || !config.auth.pass) {
|
||||
throw new Error('Missing SMTP configuration. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env');
|
||||
}
|
||||
|
||||
return nodemailer.createTransport(config);
|
||||
}
|
||||
|
||||
// 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 || process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||
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) {
|
||||
const fs = require('fs');
|
||||
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: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||
to: process.env.SMTP_USER, // Send to self
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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']) {
|
||||
const fs = require('fs');
|
||||
options.subject = fs.readFileSync(options['subject-file'], 'utf8').trim();
|
||||
}
|
||||
|
||||
// Read body from file if specified
|
||||
if (options['body-file']) {
|
||||
const fs = require('fs');
|
||||
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']) {
|
||||
const fs = require('fs');
|
||||
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;
|
||||
|
||||
default:
|
||||
console.error('Unknown command:', command);
|
||||
console.error('Available commands: send, test');
|
||||
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();
|
||||
Reference in New Issue
Block a user