feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)

Real IMAP connection via imapflow library:
- Connects to IMAP server (IMAPFlow, TLS on port 993)
- Fetches recent messages (configurable limit, default 20)
- Extracts: from, fromEmail, subject, date, snippet (200 chars)
- Thread-to-contact matching (PRD §6.1):
  1. Match sender email against persons table
  2. If no person match, extract domain and match against company.website
  3. Returns matchedCompany + matchedPerson for each email
- Audit-logged (action: crm.mailsync:imap)
- Zod validated (host, port, user, password, folder, limit)
- 502 on IMAP connection failure with detail
- 400 on validation failure

Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
Z User committed 2026-10-27 22:50:50 +00:00
1 parent 7acc0c5451
commit e45b68bb8b
1089 files changed
+260614

No files matched your search

+227
View File
@@ -0,0 +1,227 @@
## Resume / CV Template (ATS-Friendly)
Single-column, clean layout optimised for Applicant Tracking Systems (ATS). No graphics, no sidebars, no colour blocks - just well-structured text that parses perfectly by HR software.
**When to use this template:**
- Applying to corporate jobs through online portals
- Any scenario where the PDF will be machine-parsed before a human reads it
- When the recruiter explicitly asks for "a standard resume"
**When NOT to use (use Academic brief instead):**
- Creative/design industry positions → Academic brief (AltaCV-style)
- Academic CV with publications → Academic brief (Academic CV)
### Resume Design Rules
- **Target 1 page** unless user specifies otherwise
- **Margins**: `left=1.5cm, right=1.5cm, top=1.5cm, bottom=1.5cm`
- **No cover page** - content starts immediately
- **No TOC** - too short for table of contents
- **Font**: FreeSerif (English) or Noto Sans SC (Chinese)
- **Body font size**: 10-10.5pt; Name: 22-26pt; Section titles: 13-14pt
- **⚠️ Minimum font size: 12px (9pt) - HARD FLOOR.** No text in the entire resume may render smaller than 12px. This includes contact info, meta text, footnotes, and captions. Anything below 12px is unreadable in print and fails accessibility checks.
- **Section separator**: thin horizontal rule (`HRFlowable`)
- **Bullet style**: `•` with tight spacing
### Resume Line-Break Rules (Language-Aware)
- **English**: Prefer breaking at word boundaries (spaces, hyphens). If a long word must be split to avoid excessive whitespace, break at a valid syllable boundary and insert a hyphen (`-`) - this is standard typographic practice (e.g., `experi-\nence`, `develop-\nment`). ReportLab supports `wordWrap='CJK'` only for CJK content; for English use default paragraph wrapping with `allowWidows=0, allowOrphans=0`.
- **Chinese/CJK**: Break allowed between any two CJK characters. Never break between a CJK character and its adjacent punctuation (、。,)》 etc. must stay with the preceding character.
- **Mixed content** (e.g., "Python 开发工程师"): Break at CJK boundaries or English word boundaries. Never split an English word in a CJK paragraph unless hyphenated.
- **Contact line**: Email, phone, location separated by `|` or `·`. Each segment must stay on one line - if too long, move to next line at the separator, not mid-segment.
- **Dates and ranges**: "Jan 2022 - Present" must stay as one unit. Never break a date range across lines.
### Resume Page-Fill Rules (Anti-Blank-Space)
- **Goal: Fill ≥85% of the page height.** Content should reach at least the bottom quarter of the page. A resume that stops at 60% height with blank space below = FAIL.
- **Adaptive spacing strategy** (apply in order until page is ≥85% filled):
1. Increase `spaceBefore` / `spaceAfter` on section headers (from 10pt up to 18pt)
2. Increase `leading` (line height) on body text (from 14pt up to 18pt)
3. Increase body `fontSize` by 0.5-1pt (from 10pt up to 11.5pt max)
4. Add a "Professional Summary" or "Key Achievements" section if none exists
5. Increase margins slightly (from 1.5cm up to 2cm) to reduce line width and push content downward
- **Never leave a visible blank area > 3cm at the bottom of the page.**
- **If content overflows to page 2**: do the reverse - reduce spacing, tighten leading (min 12pt), reduce fontSize (min 9pt / 12px), before removing content.
### Complete Resume Template
```python
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, HRFlowable, Table, TableStyle
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.pdfmetrics import registerFontFamily
# ── Font Registration ──
pdfmetrics.registerFont(TTFont('FreeSerif', '/usr/share/fonts/truetype/freefont/FreeSerif.ttf'))
pdfmetrics.registerFont(TTFont('FreeSerif-Bold', '/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf'))
pdfmetrics.registerFont(TTFont('FreeSerif-Italic', '/usr/share/fonts/truetype/freefont/FreeSerifItalic.ttf'))
pdfmetrics.registerFont(TTFont('FreeSerif-BoldItalic', '/usr/share/fonts/truetype/freefont/FreeSerifBoldItalic.ttf'))
registerFontFamily('FreeSerif', normal='FreeSerif', bold='FreeSerif-Bold', italic='FreeSerif-Italic', boldItalic='FreeSerif-BoldItalic')
# For Chinese resumes, also register Noto Sans SC:
# pdfmetrics.registerFont(TTFont('Noto Sans SC', '/usr/share/fonts/truetype/chinese/NotoSansSC-Regular.ttf'))
# pdfmetrics.registerFont(TTFont('Noto Sans SC Bold', '/usr/share/fonts/truetype/chinese/NotoSansSC-Bold.ttf'))
# registerFontFamily('Noto Sans SC', normal='Noto Sans SC', bold='Noto Sans SC Bold')
# ── Styles ──
# ACCENT must come from palette.generate (Step 2)
# Run: python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.generate --title "Resume" --mode minimal
ACCENT = colors.HexColor('<accent from palette>') # Replace with palette output
name_style = ParagraphStyle(
'ResumeName', fontName='FreeSerif', fontSize=24,
leading=28, alignment=TA_CENTER, spaceAfter=2
)
contact_style = ParagraphStyle(
'ResumeContact', fontName='FreeSerif', fontSize=10,
leading=14, alignment=TA_CENTER, textColor=TEXT_MUTED, # From palette --c-muted
spaceAfter=8
)
section_title_style = ParagraphStyle(
'ResumeSectionTitle', fontName='FreeSerif', fontSize=13,
leading=16, spaceBefore=10, spaceAfter=4,
textColor=ACCENT
)
job_title_style = ParagraphStyle(
'ResumeJobTitle', fontName='FreeSerif', fontSize=11,
leading=14, spaceAfter=1
)
job_meta_style = ParagraphStyle(
'ResumeJobMeta', fontName='FreeSerif', fontSize=10,
leading=13, textColor=TEXT_MUTED, spaceAfter=4 # From palette --c-muted
)
bullet_style = ParagraphStyle(
'ResumeBullet', fontName='FreeSerif', fontSize=10,
leading=14, leftIndent=14, bulletIndent=0,
spaceBefore=1, spaceAfter=1
)
body_style = ParagraphStyle(
'ResumeBody', fontName='FreeSerif', fontSize=10,
leading=14, spaceAfter=2
)
# ── Helpers ──
def section_header(title):
"""Section title + thin rule separator."""
return [
Paragraph(f'<b>{title}</b>', section_title_style),
HRFlowable(width='100%', thickness=0.8, color=ACCENT,
spaceBefore=0, spaceAfter=6),
]
def experience_entry(title, company, dates, location, bullets):
"""One work experience block."""
elements = [
Paragraph(f'<b>{title}</b>', job_title_style),
Paragraph(f'{company} | {dates} | {location}', job_meta_style),
]
for b in bullets:
elements.append(Paragraph(f'• {b}', bullet_style))
elements.append(Spacer(1, 4))
return elements
def education_entry(degree, school, dates, details=None):
"""One education block."""
elements = [
Paragraph(f'<b>{degree}</b>', job_title_style),
Paragraph(f'{school} | {dates}', job_meta_style),
]
if details:
elements.append(Paragraph(details, body_style))
elements.append(Spacer(1, 4))
return elements
def skills_row(categories):
"""
Skills as compact label: value pairs.
categories = [('Programming', 'Python, Java, C++'), ('Tools', 'Git, Docker')]
"""
elements = []
for cat, vals in categories:
elements.append(Paragraph(f'<b>{cat}:</b> {vals}', body_style))
return elements
# ── Build Document ──
doc = SimpleDocTemplate(
'resume.pdf', pagesize=A4,
leftMargin=1.5*cm, rightMargin=1.5*cm,
topMargin=1.5*cm, bottomMargin=1.5*cm,
title='Resume - Your Name',
author='Z.ai', creator='Z.ai'
)
story = []
# Header
story.append(Paragraph('<b>YOUR NAME</b>', name_style))
story.append(Paragraph(
'email@example.com | +86 138-0000-0000 | Shanghai, China | github.com/yourname',
contact_style
))
# Summary
story.extend(section_header('PROFESSIONAL SUMMARY'))
story.append(Paragraph(
'Results-driven software engineer with 5+ years of experience in backend systems, '
'distributed computing, and cloud-native architectures. Led a team of 8 engineers '
'delivering a real-time data pipeline processing 2M+ events/sec.',
body_style
))
# Experience
story.extend(section_header('WORK EXPERIENCE'))
story.extend(experience_entry(
'Senior Software Engineer', 'Tech Company Inc.', 'Jan 2022 - Present', 'Shanghai',
[
'Designed and deployed a microservices architecture serving 10M daily active users',
'Reduced API latency by 40% through query optimisation and caching strategies',
'Mentored 3 junior engineers; established code review standards adopted team-wide',
]
))
story.extend(experience_entry(
'Software Engineer', 'Startup Co.', 'Jul 2019 - Dec 2021', 'Beijing',
[
'Built real-time recommendation engine using collaborative filtering (CTR +25%)',
'Implemented CI/CD pipeline reducing deployment time from 2 hours to 15 minutes',
]
))
# Education
story.extend(section_header('EDUCATION'))
story.extend(education_entry(
'M.Sc. Computer Science', 'Tsinghua University', '2017 - 2019',
'GPA: 3.8/4.0 | Thesis: Distributed Graph Processing on Heterogeneous Clusters'
))
story.extend(education_entry(
'B.Eng. Software Engineering', 'Zhejiang University', '2013 - 2017'
))
# Skills
story.extend(section_header('SKILLS'))
story.extend(skills_row([
('Languages', 'Python, Java, Go, SQL, TypeScript'),
('Frameworks', 'Spring Boot, FastAPI, React, Kubernetes, Kafka'),
('Tools', 'Git, Docker, Terraform, AWS (EC2/S3/Lambda), PostgreSQL, Redis'),
]))
doc.build(story)
```
### Resume Checklist
- [ ] **1 page** (unless user says otherwise)
- [ ] **No cover page, no TOC**
- [ ] Tight margins (1.5cm all sides)
- [ ] Name prominent at top (22-26pt)
- [ ] Contact info single line, centered
- [ ] Section headers with consistent separator style
- [ ] Bullets concise - start with action verbs
- [ ] Quantified achievements (%, $, count)
- [ ] No photos, no icons, no colour blocks (ATS-safe)
- [ ] Font: only registered fonts (FreeSerif / Noto Sans SC)
- [ ] **⚠️ Minimum font size 12px (9pt)** - no text smaller than this anywhere
- [ ] **Line breaks are language-aware** - no mid-word English breaks, no CJK punctuation orphans, no date range splits
- [ ] **Page fill ≥85%** - no large blank area at bottom. If sparse, increase spacing/leading/font size adaptively