MCP Notmuch Sendmail
by runekaagaard
import tempfile, subprocess, hashlib, mimetypes, json, os
from pathlib import Path
from typing import Optional, Dict, List
from core import BASE_PATH
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
import markdown_it
from mdit_py_plugins.deflist import deflist_plugin
from mdit_py_plugins.footnote import footnote_plugin
from mdit_py_plugins.tasklists import tasklists_plugin
from bs4 import BeautifulSoup
from jinja2 import Environment, FileSystemLoader
### Constants ###
from core import SENDMAIL_FROM_EMAIL, SENDMAIL_EMAIL_SIGNATURE_HTML
MARKDOWN_IT_FEATURES = ["table", "strikethrough"]
MARKDOWN_IT_PLUGINS = [deflist_plugin, footnote_plugin, tasklists_plugin]
### Core Functions ###
def create_draft(markdown_text: str, metadata: Dict, thread_info: Optional[Dict] = None) -> Dict:
"""Creates a draft from markdown content and metadata."""
drafts_dir = Path('drafts')
drafts_dir.mkdir(exist_ok=True)
md_path = drafts_dir / 'draft.md'
md_path.write_text(markdown_text)
css_path = BASE_PATH / 'latex.css'
html, images = markdown_to_html(markdown_text, css_path=css_path, metadata=metadata)
html_path = drafts_dir / 'draft.html'
html_path.write_text(html)
metadata_path = drafts_dir / 'draft.json'
metadata_path.write_text(json.dumps(metadata, indent=2))
return {'markdown': md_path, 'html': html_path, 'metadata': metadata_path, 'images': images}
def markdown_to_html(markdown_text: str, css_path: Optional[Path] = None, extra_options: Optional[Dict] = None,
metadata: Optional[Dict] = None) -> tuple[str, Dict]:
"""Convert markdown content to HTML with optional CSS styling."""
css = css_path.read_text() if css_path else ''
md = markdown_it.MarkdownIt('commonmark', {'html': True})
for feature in MARKDOWN_IT_FEATURES:
md = md.enable(feature)
for plugin in MARKDOWN_IT_PLUGINS:
md = md.use(plugin)
html_content = md.render(markdown_text)
images = {}
soup = BeautifulSoup(html_content, "html.parser")
imgs = soup.findAll('img')
for img in imgs:
src = img['src']
if src.startswith('data:'):
continue
elif not src.startswith('http'):
content_id = f"{hashlib.md5(src.encode('utf-8')).hexdigest()[:6]}_{Path(src).name}"
img_path = BASE_PATH / src
if img_path.exists():
images[content_id] = img_path
img['src'] = f'cid:{content_id}'
html_content = str(soup)
# Setup Jinja2 environment
env = Environment(loader=FileSystemLoader('.'))
template = env.get_template('email_template_draft.j2' if metadata else 'email_template.j2')
# Render the template
full_html = template.render(content=html_content, css=css, metadata=metadata,
signature=SENDMAIL_EMAIL_SIGNATURE_HTML)
return full_html, images
def compose(subject: str, body_as_markdown: str, to: List[str], cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None, thread_id: Optional[str] = None) -> str:
"""Create an HTML email draft from markdown content, optionally as a reply to a thread"""
thread_info = None
if thread_id:
from notmuchlib import get_thread_info
thread_info = get_thread_info(thread_id)
metadata = {
'subject': subject,
'to': to,
'cc': cc or [],
'bcc': bcc or [],
'thread_info': thread_info
}
draft = create_draft(
markdown_text=body_as_markdown,
metadata=metadata)
return f"Created drafts:\n- {draft['markdown']} (edit this)\n- {draft['html']} (preview)"
def send():
"""Send the previously composed email draft"""
drafts_dir = Path('drafts')
md_path = drafts_dir / 'draft.md'
metadata_path = drafts_dir / 'draft.json'
if not md_path.exists() or not metadata_path.exists():
raise ValueError("No draft found - compose an email first")
body_as_markdown = md_path.read_text()
metadata = json.loads(metadata_path.read_text())
css_path = BASE_PATH / 'latex.css'
html, images = markdown_to_html(body_as_markdown, css_path=css_path)
# Create email message
msg = MIMEMultipart('alternative')
# Add threading headers if this is a reply
if 'thread_info' in metadata and metadata['thread_info']:
thread_info = metadata['thread_info']
if thread_info['references']:
msg['References'] = thread_info['references']
if thread_info['message_id']:
msg['References'] = f"{msg['References']} {thread_info['message_id']}"
elif thread_info['message_id']:
msg['References'] = thread_info['message_id']
if thread_info['message_id']:
msg['In-Reply-To'] = thread_info['message_id']
msg['From'] = SENDMAIL_FROM_EMAIL
msg['To'] = ', '.join(metadata['to'])
if metadata['cc']:
msg['Cc'] = ', '.join(metadata['cc'])
if metadata['bcc']:
msg['Bcc'] = ', '.join(metadata['bcc'])
msg['Subject'] = metadata['subject']
msg_related = MIMEMultipart('related')
msg_related.attach(MIMEText(html, 'html'))
if images:
for cid, image_path in images.items():
with open(image_path, 'rb') as img:
mime_type = mimetypes.guess_type(image_path)[0]
maintype, subtype = mime_type.split('/')
img_data = img.read()
image = MIMEImage(img_data, _subtype=subtype)
image.add_header('Content-ID', f'<{cid}>')
image.add_header('Content-Disposition', 'inline')
msg_related.attach(image)
msg.attach(msg_related)
try:
with tempfile.NamedTemporaryFile(mode='w+') as tmp:
tmp.write(msg.as_string())
tmp.flush()
subprocess.run(['sendmail', '-t'], input=Path(tmp.name).read_text(), text=True, check=True,
capture_output=True)
return "Email sent successfully"
except subprocess.CalledProcessError as e:
return f"Error sending email: {e.stderr}"