Initial commit

This commit is contained in:
2026-03-20 10:28:28 +08:00
commit 1b4d5a277f
30 changed files with 14869 additions and 0 deletions

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
# Email DLP Environment Configuration
# The base URL for the OpenAI-compatible API (e.g., vLLM or OpenAI)
# If not set, it will default to the CLI argument --endpoint (defaulting to http://localhost:8000/v1)
OPENAI_BASE_URL=http://localhost:8000/v1
# The API key for the LLM service.
# For local vLLM, this is typically "not-needed" or any string.
OPENAI_API_KEY=not-needed
# The model name to use for analysis (this can also be passed via --model)
MODEL_NAME=Qwen/Qwen3.5-35B-A3B

221
.gitignore vendored Normal file
View File

@ -0,0 +1,221 @@
outputs/
preview
results/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.11

99
README.md Normal file
View File

@ -0,0 +1,99 @@
# Email DLP
An Email Data Loss Prevention (DLP) proof-of-concept tool that scans `.eml` files for policy violations using local or remote Large Language Models (LLMs). It extracts email content, parses and converts attachments, and evaluates the consolidated data against security policies to determine data risk levels and remediation actions (`BLOCK`, `ALERT`, or `PASS`).
## Features
- **Parse & Extract**: Parses raw MIME `.eml` files to extract metadata (Sender, Recipient, Subject, Date) and body text.
- **Deep Attachment Conversion**: Automatically extracts and converts attachments into text representations. Now includes extraction of images from **PDF**, **Office** documents, and supports **.zip** / **.7z** archives.
- **Heuristic Policy Review**: A fast, deterministic local engine that evaluates content against signals derived directly from `policy.py` (PII, Financial, Source Code, etc.) using keyword matching and context boosts.
- **Preview Mode**: Inspect the parsed structure, metadata, and extracted text of emails and attachments without making any LLM calls.
- **Simulation Mode**: Run a fast, deterministic local simulation of the DLP analysis without hitting an external LLM, which is extremely useful for local testing or CI environments.
- **LLM-Powered Analysis**: Conduct full DLP analysis by passing the extracted content and a configured policy prompt to an OpenAI-compatible LLM endpoint (defaulting to local vLLM).
## Installation
This project requires **Python 3.11+** and relies on [`uv`](https://github.com/astral-sh/uv) for dependency management.
```bash
# Install dependencies
uv sync
```
## Configuration
You can use a `.env` file to manage your LLM credentials and endpoint. Copy the provided example to get started:
```bash
cp .env.example .env
```
The supported environment variables are:
- `OPENAI_BASE_URL`: The API endpoint (e.g., `http://localhost:8000/v1`).
- `OPENAI_API_KEY`: Your API key (use `not-needed` for local vLLM).
- `MODEL_NAME`: The model name to use for analysis (e.g. `Qwen/Qwen3.5-35B-A3B`).
If these variables are set in `.env`, they will be used as defaults, although you can still override them using CLI flags like `--endpoint` and `--model`.
## Usage
The CLI is built with Typer and provides three main commands: `preview`, `simulate`, and `analyze`.
You can view the main help menu via:
```bash
uv run email-dlp --help
```
### 1. Preview
Preview the parsed email content, converted attachment text, and the system prompt that would be sent to the LLM.
```bash
uv run email-dlp preview \
--input data \
--output preview-output \
--include-system-prompt
```
### 2. Simulate
Execute a local simulation of the DLP parsing and analysis pipeline. This skips the LLM call entirely and returns mock evaluation results, outputting to directories like `output/<timestamp>`.
```bash
uv run email-dlp simulate \
--input data \
--output output/simulated \
--summary
```
### 3. Analyze
Perform real DLP evaluation using an LLM. By default, it targets a local OpenAI-compatible endpoint. It calculates risk scores, identifies specific violation types, and determines policy actions.
```bash
uv run email-dlp analyze \
--input data \
--output output/llm-preds \
--endpoint "http://localhost:8000/v1" \
--model "Qwen/Qwen3.5-35B-A3B" \
--summary
```
## Outputs
Analysis and Simulation runs will create `.json` files in your specified output directory for every analyzed `.eml` file.
Additionally, a `batch_summary.json` will be generated, containing:
- Total number of emails processed
- Breakdown of actions taken (`BLOCK`, `ALERT`, `PASS`)
- Distribution of identified risk levels (`CRITICAL`, `HIGH`, `MEDIUM`, `LOW`)
- Array of high-level email summaries with hit properties.
## Architecture Highlights
- Uses `markitdown`, `py7zr`, and `PyMuPDF` for deep file-parsing, archive unpacking, and image extraction.
- Validation and schema enforcement is handled via `pydantic`.
- Rich console outputs (spinners, progress bars, and formatted tables) using `rich`.

Binary file not shown.

BIN
assets/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,232 @@
Received: from BWNEWEXCH02.boardware.com.mo (172.16.21.3) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:17:43 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH02.boardware.com.mo (172.16.21.3) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:17:43 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:17:43 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: CONFIDENTIAL: Atlas Decision Engine (ADE) Source Code & Weights for
Review
Thread-Topic: CONFIDENTIAL: Atlas Decision Engine (ADE) Source Code & Weights
for Review
Thread-Index: Ady2t0CF7XHYINjSTO6QdZ/RHtjunQ==
Date: Wed, 18 Mar 2026 17:17:43 +0800
Message-ID: <da48674352a34cd196ebe59c35caaed4@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <da48674352a34cd196ebe59c35caaed4@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: 74ae67aa-6b49-4793-f714-08de84cf3634
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.3666690
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856699077_3802436304"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856699077_3802436304
Content-type: multipart/alternative;
boundary="B_3856699077_193978940"
--B_3856699077_193978940
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: 7bit
Hi Team,
Please find below the code logic for the Atlas Decision Engine (ADE) as requested. This contains the core underwriting and risk-assessment system used to compute an application's outcome.
Important: This code is Copyright (c) 2018-2025 Hyperbolic Mortgage All Rights Reserved. Do not share outside the project group.
The attachment is the snippet regarding the scoring aggregator and proprietary weights, please review the attached file.
We also need to discuss how we handle applicant_id and credit_score data in the ADEInputs class when transferring to the external API.
Please confirm receipt.
Best regards,
Dev Team
--B_3856699077_193978940
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"Segoe UI Emoji";
panose-1:2 11 5 2 4 2 4 2 2 3;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hi Team,<span style=3D"mso-fareast-language:ZH-TW"><o:p>=
</o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Please find below the <span style=3D"mso-fareast-languag=
e:ZH-TW">
code</span> logic for the <b>Atlas Decision Engine (ADE)</b> as requested. =
This contains the core underwriting and risk-assessment system used to compu=
te an application's outcome.<span style=3D"mso-fareast-language:ZH-TW"><o:p></=
o:p></span></p>
<p class=3D"MsoNormal"><b><span style=3D"font-family:&quot;Segoe UI Emoji&quot;=
,sans-serif;mso-fareast-language:ZH-TW"><o:p>&nbsp;</o:p></span></b></p>
<p class=3D"MsoNormal"><b>Important:</b> This code is <b>Copyright (c) 2018-2=
025 Hyperbolic Mortgage All Rights Reserved.</b> Do not share outside the pr=
oject group.<o:p></o:p></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">The attachmen=
t </span>
is the snippet regarding the scoring aggregator and proprietary weights<spa=
n style=3D"mso-fareast-language:ZH-TW">, please review the attached file.<o:p>=
</o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">We also need to discuss how we handle applicant_id and=
credit_score data in the ADEInputs class when transferring to the external =
API.<o:p></o:p></p>
<p class=3D"MsoNormal">Please confirm receipt.<span style=3D"mso-fareast-langua=
ge:ZH-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Best regards, <span style=3D"mso-fareast-language:ZH-TW"=
><o:p></o:p></span></p>
<p class=3D"MsoNormal">Dev Team<o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856699077_193978940--
--B_3856699077_3802436304
Content-type: application/octet-stream; name="Internal Source Code.7z";
x-mac-creator="4F50494D"
Content-disposition: attachment;
filename="Internal Source Code.7z"
Content-transfer-encoding: base64
N3q8ryccAASj2z8qUAcAAAAAAAB6AAAAAAAAAAgRyoXgFgUHSF0AEWf+iOjZxRHg1/kHV5jK
vo8Q+3+MCKFr1uKveLu7C7OlAiygp+Q6bKFk9WsmJ5DNne3sOTjCWHh0boHIEAWW4cC+BxoI
iEA/XL554i66+QYyksqGSxrUbx3TH2dlAwDaZ2nexlSC9OHNPkKe76nwlDqrui1VyjBq0pnR
wuBze46kbZc5htBhRFNLRQnGgVM9ihiOi4aWIcNofIF0Tix05kuZo2OhJzquhDtTDaa0FKbo
Dni09OwHtwwLRnA4aoFhpuaTydPwzwPosi3os3mgdVd3c2ancMuA4wbHzs0cAToJrGxFEaCz
O088euU9JOu8qvScWA670cvhy+NBdBCDZwGQktMUAH+cl+j6Me0/cjk8FfJHSDvJ1mYebujM
X9hCjhOl+iBh1Wmrfm3qMy6dePdP0HAwnCA87lVodTK3OQy1voC5xrGdBC2uiswkn2Wv8LwU
gYz/3XZ7QYRYhgwRfszWGRoREuAxwTSHJ7X6ir5ViZDXJMNbMcDJElk0F6L/79EN7fFfCzhr
fzg2uN6VV5Q4ENQ++BhL/iWdg1RgmlX9TeDyxjAbvMPnYCk85ptOiJ5olco9vcivNl7zw7Sz
MpkrnAjsVSRKhEImJ1EaKi4XC5lpVEdf0kilwow8A5N09kJU4xNtzcAfBC2GRpLn1AvcMTGp
AFbgoydLAt2jR9rToSJM5Rh9MsSNf7ucpHgrA7l3JohIATdaqbqe1uRJGWCWyez8odr0HpB/
7DHYOgIFyRADZU6MM1Rfs7rjwUrBTiERyvY0fIa25mHRieuiT2yHrTc6DK7kLjqhu7CM/Ns8
ARZOsdWsb5qRAMM1V32I+8rW278MdOn5vJRlJzGXFuJ/rccTBEmWaIVXTkvtwFJTTaycUO/0
ke/2WPYRn3XdairyD+8qDyv+ymxR8oEPfXFKnRsWkASSGI4q16twF5VkOhRjRurAMzjZgu7z
bLiTkdRbtw60qCruv0mIuhtAkQe/ET+hmNNelOpVYqedBszd6pIjh2oueuo3neYEnBYArf9L
6NozikCT4i08D7cYq272AmsA1wVLhvfdjYrJfi8dL8TrCsHnYeUe0UUjUwi5CpNFXU7UOppW
8hHZonMhDD7Bjw/i4+w4gTlxLTmLwcBYVig9IITnOReUm4G16MOJGbYc1+UH2rGk5SjE4v+O
FQtjUhVWYPqRs3Q7rmjr5piovIsKg2EOvEBssYnZFKKWnlX4IFW6o2bXK0yr+UipuwMybbeN
SUlSE5D3AIUECMt9znGPQTpXeCccY6EWx4+30KQ8hy13diGg1dGfycs33ACTa5QuJ2bK1yEV
uIifIZpiEppwbrOWX8jY+8iQuOfyNrqWZVbsOmsb73hOGbTL5X3TKamPXuf6T77/JTp6Cp5R
ODXzIpIvJC8RZRPvMMMjZKm4tWeW/1M4zx466szZmxm8iQ1tv3x+gw2gie0tmRz9y88Y/vMw
rmaYfIOOroGJAxrm3O/DNRxJ2odP0YPd19YkM8LGgW2QEJ/+7ygV6deJRuZ78onoS0RbhuEo
YQrGFHziafJDa2CnlDXiPA/L/9rEPHE+CnWRNf6Lo1aBfUJm6DXRMDKJAii8mxG1KN3VWtCl
h2rfYqoVmrVAlBotKSBmo2NmzuVFp4UOGa9/5dWD8nKF2rx0bl7cf87obNpNKK+cTGfU7qaF
zA08fNINWeqHwbl/e1O4IZwD6vBUXYwh+HQWJT3GzWbmR8e+HvO8Eveb5k6Qty11+NNMUJFN
5psFSXZ5vpuaZUSOm8/OUihUZj+WwHw+FEUqqqo3f1S6m9okPT+ivmtJfFS+zIBUKHrt1coO
YLsg3/M9zAlO8n6MThqHVHyhIycEpKPjhytusV9W7upHp8BxPtTVj/mESPhJUUJND26wKJzD
IALmQX0XaKwbyUUBMG3/BjubieNTDpTFof5wVro3EeCm5VtSyLpnAplz+bONn95cF3Q94ktc
pqC1MRIp7mvLCJikmssum2kd0KiIaZDDZggF/R0GQuXZkKkH1FEfjwqz5iFbSWfea68qR2l9
X3Jk1Unb65EWGDCUoBlh2uycdghsHBuDHWMnkkFdcH24SuR+CKhsSC1lXvxXT2jUAfwLNGH6
Bf5ZKJ68hoH1mo9zpTLErgQC9UB738kLfYPMduoiJH7wrom/HiPAuuBweYppsn4sRTTAHZha
ijA/yDU/iW/kqm8c26eQpnPjv8i63Lp/Ml11tjM0Ebindn/fPa6j4sQMwRmlc8kYLbsE9q20
8doUgZUYWVJIo56F3YNAnqJ+6O1GT2Qkc+K7tGcNDhzNFspqErpoHB0AsH1AdnrLd2QfSrZz
tLs/lqcceQofTG/3cLm9DDelKpYPWXwSf3ZhcvfMV7TskD7iMkKRC3OoZqASf37oHeZOtV3Z
7u2k51N9M51s0JEYHtPAzdsPDU2AHIh3PBdPE1MnxpeUWQJWInru7nkZeGzfhD0fSAWzCdCp
QVZmJjfF8CSwB2ewzQABBAYAAQmHUAAHCwEAASEhAQEMlgYACAoBt3c2QAAABQEZCgAAAAAA
AAAAAAARMQBJAG4AdABlAHIAbgBhAGwAIABTAG8AdQByAGMAZQAgAEMAbwBkAGUALgBwAHkA
AAAZAgAAFAoBAJwC0Hi1ttwBFQYBACAAAAAAAA==
--B_3856699077_3802436304--

View File

@ -0,0 +1,255 @@
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:08:06 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:08:06 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:08:05 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: (CONFIDENTIAL) Executed NDA: Hyperbolic Mortgage & Hyperbolic
Construction
Thread-Topic: (CONFIDENTIAL) Executed NDA: Hyperbolic Mortgage & Hyperbolic
Construction
Thread-Index: Ady2tq3H4KcRUr9KTCm6P5uRriepYg==
Date: Wed, 18 Mar 2026 17:08:05 +0800
Message-ID: <008300d108e94c99920f2665cba4e528@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach:
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <008300d108e94c99920f2665cba4e528@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: 4b9cd223-559a-44e0-937e-08de84cdddee
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.4007588
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/alternative;
boundary="B_3856698942_1521501271"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698942_1521501271
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: 7bit
Hi,
For your records, I am forwarding the signed Nondisclosure Agreement (the "Agreement") between Hyperbolic Mortgage (Disclosing Party) and Hyperbolic Construction (Receiving Party), dated October 23, 2025 (p. 2).
This document defines our Confidential Information and outlines our obligations to maintain it in strictest confidence for the sole benefit of the Disclosing Party (p. 1).
Please note the following key terms:
Definition: All materials stamped with "Confidential" or similar warnings are protected (p. 1).
Restriction: Access is limited to employees and contractors who have signed similar nondisclosure restrictions (p. 1).
Survival: These provisions remain in effect even after the termination of our working relationship (p. 1).
This is a proprietary legal document signed by Jared Wilson (CEO, Hyperbolic Mortgage) and Jaya Sathe (CEO, Hyperbolic Construction) (p. 2). It should not be distributed to any unauthorized third parties.
Regards,
Lukey
Legal Department
Hyperbolic Mortgage
--B_3856698942_1521501271
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:1241797232;
mso-list-template-ids:-1687115448;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hi,<span style=3D"mso-fareast-language:ZH-TW"><o:p></o:p=
></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">For your records, I am forwarding the signed&nbsp;<b>N=
ondisclosure Agreement (the &quot;Agreement&quot;)</b>&nbsp;between&nbsp;<b>=
Hyperbolic Mortgage</b>&nbsp;(Disclosing Party) and&nbsp;<b>Hyperbolic Const=
ruction</b>&nbsp;(Receiving Party), dated October 23, 2025 (p. 2).<o:p></o:p=
></p>
<p class=3D"MsoNormal">This document defines our&nbsp;<b>Confidential Informa=
tion</b>&nbsp;and outlines our obligations to maintain it in&nbsp;<b>stricte=
st confidence</b>&nbsp;for the sole benefit of the Disclosing Party (p. 1).<=
o:p></o:p></p>
<p class=3D"MsoNormal">Please note the following key terms:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>Definition:</b>&nb=
sp;All materials stamped with&nbsp;<b>&quot;Confidential&quot;</b>&nbsp;or s=
imilar warnings are protected (p. 1).<o:p></o:p></li><li class=3D"MsoNormal" s=
tyle=3D"mso-list:l0 level1 lfo1"><b>Restriction:</b>&nbsp;Access is limited to=
employees and contractors who have signed similar nondisclosure restriction=
s (p. 1).<o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo=
1"><b>Survival:</b>&nbsp;These provisions remain in effect even after the te=
rmination of our working relationship (p. 1).<o:p></o:p></li></ul>
<p class=3D"MsoNormal">This is a&nbsp;<b>proprietary legal document</b>&nbsp;=
signed by&nbsp;<b>Jared Wilson (CEO, Hyperbolic Mortgage)</b>&nbsp;and&nbsp;=
<b>Jaya Sathe (CEO, Hyperbolic Construction)</b>&nbsp;(p. 2). It should not =
be distributed to any unauthorized third parties.<o:p></o:p></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Regards,<o:p></o:p></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey</span><=
br>
Legal Department<br>
<b>Hyperbolic Mortgage</b><o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856698942_1521501271--

View File

@ -0,0 +1,346 @@
Received: from BWNEWEXCH02.boardware.com.mo (172.16.21.3) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:01:16 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH02.boardware.com.mo (172.16.21.3) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:01:14 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:01:16 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: (ACTION REQUIRED) External Review: Hyperbolic Mortgage Internal Data
Security Policy v3.1
Thread-Topic: (ACTION REQUIRED) External Review: Hyperbolic Mortgage Internal
Data Security Policy v3.1
Thread-Index: Ady2ta8QpT+uvnWKT1yzw+Y8XK8LOQ==
Date: Wed, 18 Mar 2026 17:01:16 +0800
Message-ID: <f8410ec9fcb84e22b1873e3f4b33dda6@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <f8410ec9fcb84e22b1873e3f4b33dda6@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: 4253c263-330f-42e1-7b6b-08de84cce9e8
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.4116177
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856698953_771913467"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698953_771913467
Content-type: multipart/alternative;
boundary="B_3856698953_2299336799"
--B_3856698953_2299336799
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Hi,
=20
As discussed, I am forwarding the INTERNAL POLICY DOCUMENT for Hyperbolic M=
ortgage regarding our Data Security & Acceptable Use Policy (Effective Date:=
February 1, 2025).
This document is marked Internal Use Only =E2=80=94 Not for Public Distribution (=
p. 2). However, I am sending this to your personal account so you can review=
our internal PHI, PII, and financial records encryption standards and our c=
loud and on-prem application security protocols (p. 1).
Key sections for your review:
Customer financial data protection requirements.
Third-party SaaS platform usage guidelines.
Internal Incident Reporting procedures and security contact details (securi=
ty@hyperbolicmortgage.com).
Please ensure this remains confidential as it contains sensitive informatio=
n about our borrower documents and access-controlled systems (p. 1).
=20
Best,
Lukey
Hyperbolic Mortgage
=20
--B_3856698953_2299336799
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:1298031305;
mso-list-template-ids:-438370642;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hi,<span style=3D"mso-fareast-language:ZH-TW"><o:p></o:p=
></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">As discussed, I am forwarding the&nbsp;<b>INTERNAL POL=
ICY DOCUMENT</b>&nbsp;for&nbsp;<b>Hyperbolic Mortgage</b>&nbsp;regarding our=
&nbsp;<b>Data Security &amp; Acceptable Use Policy</b>&nbsp;(Effective Date:=
February 1, 2025).<o:p></o:p></p>
<p class=3D"MsoNormal">This document is marked&nbsp;<b>Internal Use Only =E2=80=94 =
Not for Public Distribution</b>&nbsp;(p. 2). However, I am sending this to y=
our personal account so you can review our internal&nbsp;<b>PHI, PII, and fi=
nancial records</b>&nbsp;encryption standards and our&nbsp;<b>cloud
and on-prem application</b>&nbsp;security protocols (p. 1).<o:p></o:p></p>
<p class=3D"MsoNormal">Key sections for your review:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>Customer financial=
data</b>&nbsp;protection requirements.<o:p></o:p></li><li class=3D"MsoNormal"=
style=3D"mso-list:l0 level1 lfo1"><b>Third-party SaaS platform</b>&nbsp;usage=
guidelines.<o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 =
lfo1">Internal&nbsp;<b>Incident Reporting</b>&nbsp;procedures and security c=
ontact details (security@hyperbolicmortgage.com).<o:p></o:p></li></ul>
<p class=3D"MsoNormal">Please ensure this remains confidential as it contains=
sensitive information about our&nbsp;<b>borrower documents</b>&nbsp;and&nbs=
p;<b>access-controlled systems</b>&nbsp;(p. 1).<span style=3D"mso-fareast-lang=
uage:ZH-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Best,<o:p></o:p></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey</span><=
br>
<b>Hyperbolic Mortgage</b><o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856698953_2299336799--
--B_3856698953_771913467
Content-type: application/pdf; name="Policy File.pdf";
x-mac-creator="4F50494D";
x-mac-type="50444620"
Content-disposition: attachment;
filename="Policy File.pdf"
Content-transfer-encoding: base64
JVBERi0xLjQKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUiAvRjMg
NCAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EgL0VuY29k
aW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YxIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAv
Rm9udAo+PgplbmRvYmoKMyAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYS1Cb2xkIC9F
bmNvZGluZyAvV2luQW5zaUVuY29kaW5nIC9OYW1lIC9GMiAvU3VidHlwZSAvVHlwZTEgL1R5
cGUgL0ZvbnQKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL0Jhc2VGb250IC9aYXBmRGluZ2JhdHMg
L05hbWUgL0YzIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKNSAwIG9i
ago8PAovQ29udGVudHMgMTAgMCBSIC9NZWRpYUJveCBbIDAgMCA2MTIgNzkyIF0gL1BhcmVu
dCA5IDAgUiAvUmVzb3VyY2VzIDw8Ci9Gb250IDEgMCBSIC9Qcm9jU2V0IFsgL1BERiAvVGV4
dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSSBdCj4+IC9Sb3RhdGUgMCAvVHJhbnMgPDwKCj4+
IAogIC9UeXBlIC9QYWdlCj4+CmVuZG9iago2IDAgb2JqCjw8Ci9Db250ZW50cyAxMSAwIFIg
L01lZGlhQm94IFsgMCAwIDYxMiA3OTIgXSAvUGFyZW50IDkgMCBSIC9SZXNvdXJjZXMgPDwK
L0ZvbnQgMSAwIFIgL1Byb2NTZXQgWyAvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1h
Z2VJIF0KPj4gL1JvdGF0ZSAwIC9UcmFucyA8PAoKPj4gCiAgL1R5cGUgL1BhZ2UKPj4KZW5k
b2JqCjcgMCBvYmoKPDwKL1BhZ2VNb2RlIC9Vc2VOb25lIC9QYWdlcyA5IDAgUiAvVHlwZSAv
Q2F0YWxvZwo+PgplbmRvYmoKOCAwIG9iago8PAovQXV0aG9yIChcKGFub255bW91c1wpKSAv
Q3JlYXRpb25EYXRlIChEOjIwMjUxMTIxMDIzODUwKzAwJzAwJykgL0NyZWF0b3IgKFwodW5z
cGVjaWZpZWRcKSkgL0tleXdvcmRzICgpIC9Nb2REYXRlIChEOjIwMjUxMTIxMDIzODUwKzAw
JzAwJykgL1Byb2R1Y2VyIChSZXBvcnRMYWIgUERGIExpYnJhcnkgLSB3d3cucmVwb3J0bGFi
LmNvbSkgCiAgL1N1YmplY3QgKFwodW5zcGVjaWZpZWRcKSkgL1RpdGxlIChcKGFub255bW91
c1wpKSAvVHJhcHBlZCAvRmFsc2UKPj4KZW5kb2JqCjkgMCBvYmoKPDwKL0NvdW50IDIgL0tp
ZHMgWyA1IDAgUiA2IDAgUiBdIC9UeXBlIC9QYWdlcwo+PgplbmRvYmoKMTAgMCBvYmoKPDwK
L0ZpbHRlciBbIC9BU0NJSTg1RGVjb2RlIC9GbGF0ZURlY29kZSBdIC9MZW5ndGggMTYwNwo+
PgpzdHJlYW0KR2F0JSJnTVolMCY6TWwrYlohSVA+ZCo2b10yLVxqNW4lUV4+cnU3KWE5c1RJ
Mkh1J2dQI1NhQFlPPEBvQFJrRmIoLk82UmpwXVMrRihXTEhgcVVoTDQzYFU3KEEuLFhUayRL
SidkWkFGLD9mSmxsL0xYVTYuMmhzNGVuM0otV2w9T2o3cHAlJWhQJUtSMVBRLGJDbCQ3Yzgo
ZixOYUtmJG5iRGFEKCwlTTNoKD5MMm1gbiZHX3U4Y09qZSRYO0osKz1OXWomaVg8Q2pVKz4s
Pz1WPyEqI21yJzYxVEJebW4sK3FkLmlhNiw9SVA3bi9MSFtCcDgsQ0JbXShlWGxdTTE/ZWMw
YHU2bVRZVV9LPy4jNGFqZW9WLmo8PzJwZCQ0bGdqdFxOWiQ1Tk0ta0BsRSgrSTYzWlVSK3Mj
YFtKaj05Z0FEJlNRRkNUYyQyMz9wOHNzaTxcYi1MPW43bCpKNl45by50dCNQKTpFJVgzU3Ar
X11JOTp1UTVOdC1EXEhBblBbKVZbWEs5VDYhNDZUR3BUJ1FPVjUncylqX1RER29PMitwLyI7
ajgxaz9GKGxZWj9gJC45XWJxaSgwUSxVN1VgPyIoRzVBYC5dYGBDPiwmQyxyKSZRQmc+dTpM
UElTTyFZTW1vN1IxZDhSR1RbKCRlW18jUS4maTRZVlNnYmNXQSVHcCxqTV5ARlNQSVJrP0Aq
NEVqKkR1YHBXTjdFKVdFQltVPDw7STNPIWY8WXVNWUZNKFptTTFXZy9fKXE4U1V0PStxPCxe
K20uRSpMVFE3PCo6VTciIjw/RUxcJ1o3QFBNLVU3R1Y9W2tdZ3BeYzcuXG5EcyU2PyJoND1T
cVYmO0JBVmwwMEJGYW8kJ2g/XEE2LiJYJj86TXVMK2ArQUtXWk1SdFBBT0UhM0whbTRxOm1t
WT5WKUAxY2QhZXNZSz9gQUtQcXA0UCQ7UCFHUnM7ODspOCprKkdBP1RFZCdoLy01a0VJRmxi
ZlVUXHA0T1hhJFRDL0UmK1VTWCEuMkEvUWVlSlFQIk9FMWAnVXJLYmY0T0hoTFNYYypvNzsx
PTJEXU1zS2NfUE9FSj9ULDpxcltvKjVca2pcSTtTW04uKiowZy4jalU9OmJLLlVAdD5iWio4
JVxWYTpVPWJwSjQ6Y1M8XVUhL0RTWzB1cjBQXHRjZWdUM0VFJUAuWnBkOENMQ1klXExtcEBX
VC1PMzlYbGIwdHEtYWwhXCNbTGxeW2NEOm8zQkJhSzFLbFxaK0NlSkI+TiF1Z2hULjQmZmY4
PlVUNVRDSCVpLToyYG1sKzNhay9OdDo4Pi9FRzh1Tzw4Kk4yUXJYUzNgbGxeZ1NAS09OMlNN
ZT00IiRiZDwsS2Q2cTpBbUVtazsuXUo3Q2hJa0pEL2xSUmtaR3RLJWUnYGs4LnVWMTdZZjg+
Ymk9aVtFZyFwXyZMdS5HYDBQLF8qZCE7NS04I3NVT0xdRXIxXF9FYDExVT5eQCciNEtvXF5y
K2E9KXRbPVpwIkZxWDhNRGFcXU1HS2RHNSdENUp1LiM3Iig5QztOWEp1VW1nbz1xX0lTQVxJ
b1hcXGQvXlI1UHFANWIxTG9OI0tNZSosNnJOQVJXOE1MW2Y1bjJWJz5IQFdvK0I3OVluR0c8
XWxELCVIO1InbkZwP1U3UTY5ZG1kSkE1WV9iMGJeJEBsUG9EQVVSZDEpOVFAQktmbkk3bjsw
UVBwPmg7Lk9xRTtmWEVKVTtoREpQMyMhaUhOS21MdE0kbm5oRlhLX1ZJZWo6aS05YlNaNUdX
MzdyOU9GNjojUWtgJSozLjBaKHAkZkJEQ0YrYVBxLzBzOjxabzUpQlsrWSdrJD5wRjo8N0xF
LjlxM2ImY0JBVlFaVmhhVCUsYCMkaS44K0dJSFlZZDVqVXRdUixzOU4rZ0EzOkUkLyI1IWx0
NVguJ1E8S2E5ZCdDRytUbVUiIlhxT1hablBXK3Upby1MKlpfPWlDMihvTS8lI0FCaEI4SXRu
SlE1RUl1N1whTV5LO24vRjQ+LUBCKTtnViVHOUNiZjE6TF5HNj5CRyhyIVo4PkpUKFFvTEFd
ZyYnSDs8QzEiaT1nKSZhcUpwM2VCKVhRJGdFLXA5P2tkZmpTVzVnTjtvcSlodVkwfj5lbmRz
dHJlYW0KZW5kb2JqCjExIDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxh
dGVEZWNvZGUgXSAvTGVuZ3RoIDMxMgo+PgpzdHJlYW0KR2FwcFk5MkVHWiY7OU5KJ2x0cSk5
Jmg9cjRuR1YqJ18pYXNgbDFOWFdLQ1xnTlhhLkdCNEQ4ZUgiRitsU1FUMGlAIzdrOyFQIkVJ
KiFBWTlIJWAtZSRGN1soYmcpV2RPTG1ZbltNP144QHA4ZExDXlw8LToiSScjYFNzYD9xJllY
TD1OUWJ1MEtQdWApMTsmSlY8T0kxLXU1UzVBayJSJFMtWCZfN14nX0I3U1VRT0NCY2lUXFJA
PHVmcDMxPyU5JSNdWXU/Mm8pRHVNV3AnVUBINz0iMG47OE4mZ0VHZCYqSnAoMDROXWZvIkJj
bkBsNDpOaFRiTG1sWGJNSzlpKW9zXSlxYT5cO11cPnFabWRUOT1ObT9aMjJaKnA7bFJAKTdg
NU9iOmJOQWhOQUtncE9oVGZdTn4+ZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgMTIKMDAwMDAw
MDAwMCA2NTUzNSBmIAowMDAwMDAwMDczIDAwMDAwIG4gCjAwMDAwMDAxMjQgMDAwMDAgbiAK
MDAwMDAwMDIzMSAwMDAwMCBuIAowMDAwMDAwMzQzIDAwMDAwIG4gCjAwMDAwMDA0MjYgMDAw
MDAgbiAKMDAwMDAwMDYyMCAwMDAwMCBuIAowMDAwMDAwODE0IDAwMDAwIG4gCjAwMDAwMDA4
ODIgMDAwMDAgbiAKMDAwMDAwMTE2NSAwMDAwMCBuIAowMDAwMDAxMjMwIDAwMDAwIG4gCjAw
MDAwMDI5MjkgMDAwMDAgbiAKdHJhaWxlcgo8PAovSUQgCls8NzA1NzcyZTcwMzE5OWRjMWVl
YzkxODIyNDUwMTIyOGE+PDcwNTc3MmU3MDMxOTlkYzFlZWM5MTgyMjQ1MDEyMjhhPl0KJSBS
ZXBvcnRMYWIgZ2VuZXJhdGVkIFBERiBkb2N1bWVudCAtLSBkaWdlc3QgKGh0dHA6Ly93d3cu
cmVwb3J0bGFiLmNvbSkKCi9JbmZvIDggMCBSCi9Sb290IDcgMCBSCi9TaXplIDEyCj4+CnN0
YXJ0eHJlZgozMzMyCiUlRU9GCg==
--B_3856698953_771913467--

View File

@ -0,0 +1,230 @@
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:23:21 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:23:20 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:23:20 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: URGENT: Full Customer List & Sales Data for Q1 Campaign
Thread-Topic: URGENT: Full Customer List & Sales Data for Q1 Campaign
Thread-Index: Ady2uGIzHiAOL6KDSNKITnJUWXGuTg==
Date: Wed, 18 Mar 2026 17:23:20 +0800
Message-ID: <9faf5abfe0d04fd58471a1a2b59d0cdb@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <9faf5abfe0d04fd58471a1a2b59d0cdb@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: a39bffb2-3f7c-4b2a-b42f-08de84cfff43
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.3297607
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856701037_1656718459"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856701037_1656718459
Content-type: multipart/alternative;
boundary="B_3856701037_2332483852"
--B_3856701037_2332483852
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: 7bit
Hi Team,
As discussed, please find the raw export of our top-tier prospects below. We need to launch the outreach campaign by tomorrow.
CONFIDENTIAL: This list contains PII (Personally Identifiable Information) and proprietary revenue data. Please handle with care and delete after use.
Key targets include EverTrust Commercial Bank ($6.8M sales) and Silverline Capital Advisors ($6.1M sales). Note that Isabella Martinez at Sunrise Credit Union is a high-priority lead.
Please confirm you can ingest this format.
Best regards,
Lukey
Hyperbolic Mortgage
--B_3856701037_2332483852
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hi Team,<span style=3D"mso-fareast-language:ZH-TW"><o:p>=
</o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">As discussed, please find the raw export of our top-ti=
er prospects below. We need to launch the outreach campaign by tomorrow.<o:p=
></o:p></p>
<p class=3D"MsoNormal"><b>CONFIDENTIAL:</b> This list contains <b>PII (Person=
ally Identifiable Information)</b> and proprietary revenue data. Please hand=
le with care and delete after use.<o:p></o:p></p>
<p class=3D"MsoNormal">Key targets include <b>EverTrust Commercial Bank</b> (=
$6.8M sales) and
<b>Silverline Capital Advisors</b> ($6.1M sales). Note that <b>Isabella Mar=
tinez</b> at
<b>Sunrise Credit Union</b> is a high-priority lead.<o:p></o:p></p>
<p class=3D"MsoNormal">Please confirm you can ingest this format.<span style=3D=
"mso-fareast-language:ZH-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Best regards, <span style=3D"mso-fareast-language:ZH-TW"=
><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey<o:p></o=
:p></span></p>
<p class=3D"MsoNormal"><i>Hyperbolic Mortgage</i><o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856701037_2332483852--
--B_3856701037_1656718459
Content-type: application/octet-stream; name="Customer List.csv";
x-mac-creator="4F50494D"
Content-disposition: attachment;
filename="Customer List.csv"
Content-transfer-encoding: base64
Y3VzdG9tZXJfaWQsZmlyc3RfbmFtZSxsYXN0X25hbWUsZW1haWwscGhvbmUsY29tcGFueV9u
YW1lLGNpdHksY291bnRyeSxhbm51YWxfc2FsZXNfdXNkCkNVU1QtMTAwMSxFbWlseSxUcmFu
LGVtaWx5LnRyYW5AYXVyb3JhZmluYW5jZS5pbywrMS00MTUtNTU1LTAxMjEsQXVyb3JhIEZp
bmFuY2UgSW5jLFNhbiBGcmFuY2lzY28sVVNBLDE0NTAwMDAKQ1VTVC0xMDAyLE1hcmN1cyxS
aXZlcmEsbWFyY3VzLnJpdmVyYUBub3J0aHBlYWtoZWFsdGguY29tLCsxLTYxNy01NTUtMjAy
OSxOb3J0aFBlYWsgSGVhbHRoIFN5c3RlbXMsQm9zdG9uLFVTQSwyMjg1MDAwCkNVU1QtMTAw
MyxTb2ZpYSxLbGVpbixzb2ZpYS5rbGVpbkBibHVlaG9yaXpvbnJldGFpbC5jb20sKzEtMzEy
LTU1NS04ODkwLEJsdWUgSG9yaXpvbiBSZXRhaWwgTExDLENoaWNhZ28sVVNBLDg3NTAwMApD
VVNULTEwMDQsRGF2aWQsQ2hvLGRhdmlkLmNob0B2ZXJ0ZXhhbmFseXRpY3MuY28sKzQ0LTIw
LTc5NDYtMTEyMixWZXJ0ZXggQW5hbHl0aWNzIEx0ZCxMb25kb24sVUssMTkzMDAwMApDVVNU
LTEwMDUsSXNhYmVsbGEsTWFydGluZXosaXNhLm1hcnRpbmV6QHN1bnJpc2VjcmVkaXQub3Jn
LCsxLTY0Ni01NTUtMzM5OSxTdW5yaXNlIENyZWRpdCBVbmlvbixOZXcgWW9yayxVU0EsNDEy
NTAwMApDVVNULTEwMDYsUnlhbixDb2xsaW5zLHJ5YW4uY29sbGluc0BvY2Vhbmljc2hpcHBp
bmcuaW8sKzYxLTItNTU1MC05OTgxLE9jZWFuaWMgU2hpcHBpbmcgJiBMb2dpc3RpY3MsU3lk
bmV5LEF1c3RyYWxpYSwyNjc1MDAwCkNVU1QtMTAwNyxOYWRpYSxQYXRlbCxuYWRpYS5wYXRl
bEBicmlnaHRwYXRoaW5zdXJlLmNvbSwrMS0zMDMtNTU1LTc3MTIsQnJpZ2h0UGF0aCBJbnN1
cmFuY2UgR3JvdXAsRGVudmVyLFVTQSwxMzQwMDAwCkNVU1QtMTAwOCxFdGhhbixQcmljZSxl
dGhhbi5wcmljZUBncmVlbmZpZWxkZW5lcmd5LmNvLCsxLTcxMy01NTUtNDQ5MCxHcmVlbmZp
ZWxkIEVuZXJneSBQYXJ0bmVycyxIb3VzdG9uLFVTQSw1MjUwMDAwCkNVU1QtMTAwOSxDbGFp
cmUsTmd1eWVuLGNsYWlyZS5uZ3V5ZW5AbGFrZXNpZGVwaGFybWEuY29tLCsxLTkxOS01NTUt
NjYxMixMYWtlc2lkZSBQaGFybWFjZXV0aWNhbHMsUmFsZWlnaCxVU0EsMzgyMDAwMApDVVNU
LTEwMTAsSmFzb24sTG9wZXosamFzb24ubG9wZXpAc2lsdmVybGluZWNhcGl0YWwuY29tLCsx
LTIxMi01NTUtNzQwMyxTaWx2ZXJsaW5lIENhcGl0YWwgQWR2aXNvcnMsTmV3IFlvcmssVVNB
LDYxNTAwMDAKQ1VTVC0xMDExLE1hcmlhLFJvc3NpLG0ucm9zc2lAbm92YWVsZXR0cm9uaWNh
Lml0LCszOS0wMi01NTAtODgyMixOb3ZhIEVsZXR0cm9uaWNhIFMucC5BLixNaWxhbixJdGFs
eSw5OTAwMDAKQ1VTVC0xMDEyLFRob21hcyxCYWtlcix0aG9tYXMuYmFrZXJAcmVkd29vZGxv
Z2lzdGljcy5jb20sKzEtNTAzLTU1NS02MDAxLFJlZHdvb2QgTG9naXN0aWNzIENvcnAsUG9y
dGxhbmQsVVNBLDE3MTAwMDAKQ1VTVC0xMDEzLEhhbm5haCxGaXNjaGVyLGguZmlzY2hlckBy
aGVpbnRlY2guZGUsKzQ5LTIxMS01NTUtNzcwMCxSaGVpblRlY2ggU29sdXRpb25zIEdtYkgs
RMO8c3NlbGRvcmYsR2VybWFueSwyNDMwMDAwCkNVU1QtMTAxNCxPd2VuLENsYXJrLG93ZW4u
Y2xhcmtAbWV0cm9wcm9wZXJ0aWVzLmNvLnVrLCs0NC0xNjEtNTU1LTM0MDksTWV0cm8gUHJv
cGVydGllcyBHcm91cCxNYW5jaGVzdGVyLFVLLDE1ODAwMDAKQ1VTVC0xMDE1LExpbHksQ2hl
bixsaWx5LmNoZW5AcGFjaWZpY3dlbGxuZXNzLm9yZywrMS0yMDYtNTU1LTExODgsUGFjaWZp
YyBXZWxsbmVzcyBOZXR3b3JrLFNlYXR0bGUsVVNBLDkyMDAwMApDVVNULTEwMTYsTm9haCxB
bmRlcnNvbixub2FoLmFuZGVyc29uQHN0YXJsaWdodG1lZGlhLmNvLCsxLTMxMC01NTUtNDQ3
NyxTdGFybGlnaHQgTWVkaWEgJiBFbnRlcnRhaW5tZW50LExvcyBBbmdlbGVzLFVTQSw0ODc1
MDAwCkNVU1QtMTAxNyxHcmFjZSxOYWthbXVyYSxncmFjZS5uYWthbXVyYUB0b2t5b3ZlbnR1
cmVzLmpwLCs4MS0zLTU1NTAtMzM0NCxUb2t5byBWZW50dXJlcyBLSyxUb2t5byxKYXBhbiwz
MTAwMDAwCkNVU1QtMTAxOCxKYWNvYixLaW5nLGphY29iLmtpbmdAZXZlcnRydXN0YmFuay5j
b20sKzEtNDA0LTU1NS05MzIxLEV2ZXJUcnVzdCBDb21tZXJjaWFsIEJhbmssQXRsYW50YSxV
U0EsNjgyMDAwMApDVVNULTEwMTksT2xpdmlhLFN0ZXdhcnQsb2xpdmlhLnN0ZXdhcnRAY2xv
dWRyaWRnZS50ZWNoLCsxLTY1MC01NTUtMjIxMSxDbG91ZFJpZGdlIFRlY2hub2xvZ2llcyxN
b3VudGFpbiBWaWV3LFVTQSwyNTM1MDAwCkNVU1QtMTAyMCxMaWFtLEdhcmNpYSxsaWFtLmdh
cmNpYUBzb3V0aGJheW1hbnVmYWN0dXJpbmcuY29tLCsxLTQwOC01NTUtNzczMCxTb3V0aEJh
eSBNYW51ZmFjdHVyaW5nIEluYyxTYW4gSm9zZSxVU0EsMTk4MDAwMApDVVNULTEwMjEsTWVn
YW4sSm9oYW5zc29uLG1lZ2FuLmpvaGFuc3NvbkBub3JkaWNzaGlwcGluZy5zZSwrNDYtOC01
NTUtOTA0MDAsTm9yZGljIFNoaXBwaW5nICYgRnJlaWdodCxTdG9ja2hvbG0sU3dlZGVuLDE0
MTAwMDAKQ1VTVC0xMDIyLFZpY3RvcixTaWx2YSx2aWN0b3Iuc2lsdmFAYXRsYW50aWNvYWxp
bWVudG9zLmNvbSwrNTUtMTEtNTU1MC03Nzg4LEF0bMOibnRpY28gQWxpbWVudG9zIFMuQS4s
U8OjbyBQYXVsbyxCcmF6aWwsMjc2MDAwMApDVVNULTEwMjMsWm9lLEhhcnQsem9lLmhhcnRA
aGFyYm9ydmlld2NhcGl0YWwuaW8sKzEtMzA1LTU1NS02NjAwLEhhcmJvcnZpZXcgQ2FwaXRh
bCBNYW5hZ2VtZW50LE1pYW1pLFVTQSwzMzc1MDAwCkNVU1QtMTAyNCxEYW5pZWwsS2ltLGRh
bmllbC5raW1AZ2xvYmFsY2FyZWhlYWx0aC5vcmcsKzEtMjE1LTU1NS01NDkwLEdsb2JhbENh
cmUgSGVhbHRoIE5ldHdvcmssUGhpbGFkZWxwaGlhLFVTQSw0MjUwMDAwCkNVU1QtMTAyNSxB
c2hsZXksTXVycGh5LGFzaGxleS5tdXJwaHlAcHJpbWVsZWRnZXJwYXkuY29tLCsxLTcwMi01
NTUtODg0MixQcmltZUxlZGdlciBQYXltZW50cyBJbmMsTGFzIFZlZ2FzLFVTQSwxMzIwMDAw
Cg==
--B_3856701037_1656718459--

View File

@ -0,0 +1,324 @@
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:06:08 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:06:08 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:06:08 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: (INTERNAL ONLY) Fwd: Q1 Organizational Priorities & Alignment -
Hyperbolic Mortgage
Thread-Topic: (INTERNAL ONLY) Fwd: Q1 Organizational Priorities & Alignment -
Hyperbolic Mortgage
Thread-Index: Ady2tmQoU2x85dviTqWBzyVUl6A37w==
Date: Wed, 18 Mar 2026 17:06:08 +0800
Message-ID: <ac5c3d2b496a4bdebbd64e9a1d71a5e3@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <ac5c3d2b496a4bdebbd64e9a1d71a5e3@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: a9e91c40-8108-4182-a254-08de84cd97dd
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.4281982
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856698945_1868877581"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698945_1868877581
Content-type: multipart/alternative;
boundary="B_3856698945_922363724"
--B_3856698945_922363724
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Hi,
=20
I=E2=80=99m forwarding the Internal Memo from the Office of the CEO regarding our=
Q1 Organizational Priorities for Hyperbolic Mortgage.
This memo is strictly for Internal Use Only =E2=80=94 Do Not Distribute Externall=
y, but I wanted to share our "growth roadmap" and "operational changes" for =
the upcoming quarter. Key points include:
Loan Cycle Time Reduction: Launching a company-wide efficiency initiative.
LOS Automation: New training modules for automation tools starting next mon=
th.
Workflow Reviews: Cross-functional reviews led by the Operations team to ad=
dress blockers.
This information is vital for understanding our long-term strategic success=
and competitive edge. Please do not share this outside of this email thread=
as it contains non-public operational stability details.
=20
Best,
Lukey
Hyperbolic Mortgage
=20
--B_3856698945_922363724
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:2061782407;
mso-list-template-ids:-572873242;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Hi,<o:p></o:p=
></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">I=E2=80=99m forward=
ing the&nbsp;<b>Internal Memo</b>&nbsp;from the&nbsp;<b>Office of the CEO</b=
>&nbsp;regarding our&nbsp;<b>Q1 Organizational Priorities</b>&nbsp;for&nbsp;=
<b>Hyperbolic Mortgage</b>.<o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">This memo is =
strictly for&nbsp;<b>Internal Use Only =E2=80=94 Do Not Distribute Externally</b>,=
but I wanted to share our &quot;growth roadmap&quot; and &quot;operational =
changes&quot; for the upcoming quarter. Key points include:<o:p></o:p></span=
></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b><span style=3D"mso-f=
areast-language:ZH-TW">Loan Cycle Time Reduction:</span></b><span style=3D"mso=
-fareast-language:ZH-TW">&nbsp;Launching a company-wide efficiency initiativ=
e.<o:p></o:p></span></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo=
1"><b><span style=3D"mso-fareast-language:ZH-TW">LOS Automation:</span></b><sp=
an style=3D"mso-fareast-language:ZH-TW">&nbsp;New training modules for automat=
ion tools starting next month.<o:p></o:p></span></li><li class=3D"MsoNormal" s=
tyle=3D"mso-list:l0 level1 lfo1"><b><span style=3D"mso-fareast-language:ZH-TW">W=
orkflow Reviews:</span></b><span style=3D"mso-fareast-language:ZH-TW">&nbsp;Cr=
oss-functional reviews led by the Operations team to address blockers.<o:p><=
/o:p></span></li></ul>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">This informat=
ion is vital for understanding our&nbsp;<b>long-term strategic success</b>&n=
bsp;and&nbsp;<b>competitive edge</b>. Please do not share this outside of th=
is email thread as it contains non-public&nbsp;<b>operational
stability</b>&nbsp;details.<o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Best,<o:p></o=
:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey<br>
<b>Hyperbolic Mortgage</b><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
</div>
</body>
</html>
--B_3856698945_922363724--
--B_3856698945_1868877581
Content-type: application/pdf; name="Internal_Memo_Q1_Priorities_2025.pdf";
x-mac-creator="4F50494D";
x-mac-type="50444620"
Content-disposition: attachment;
filename="Internal_Memo_Q1_Priorities_2025.pdf"
Content-transfer-encoding: base64
JVBERi0xLjQKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUgo+Pgpl
bmRvYmoKMiAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYSAvRW5jb2RpbmcgL1dpbkFu
c2lFbmNvZGluZyAvTmFtZSAvRjEgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250Cj4+CmVu
ZG9iagozIDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLUJvbGQgL0VuY29kaW5nIC9X
aW5BbnNpRW5jb2RpbmcgL05hbWUgL0YyIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+
PgplbmRvYmoKNCAwIG9iago8PAovQ29udGVudHMgOCAwIFIgL01lZGlhQm94IFsgMCAwIDYx
MiA3OTIgXSAvUGFyZW50IDcgMCBSIC9SZXNvdXJjZXMgPDwKL0ZvbnQgMSAwIFIgL1Byb2NT
ZXQgWyAvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJIF0KPj4gL1JvdGF0ZSAw
IC9UcmFucyA8PAoKPj4gCiAgL1R5cGUgL1BhZ2UKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL1Bh
Z2VNb2RlIC9Vc2VOb25lIC9QYWdlcyA3IDAgUiAvVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoK
NiAwIG9iago8PAovQXV0aG9yIChcKGFub255bW91c1wpKSAvQ3JlYXRpb25EYXRlIChEOjIw
MjUxMTIxMDIxMjUwKzAwJzAwJykgL0NyZWF0b3IgKFwodW5zcGVjaWZpZWRcKSkgL0tleXdv
cmRzICgpIC9Nb2REYXRlIChEOjIwMjUxMTIxMDIxMjUwKzAwJzAwJykgL1Byb2R1Y2VyIChS
ZXBvcnRMYWIgUERGIExpYnJhcnkgLSB3d3cucmVwb3J0bGFiLmNvbSkgCiAgL1N1YmplY3Qg
KFwodW5zcGVjaWZpZWRcKSkgL1RpdGxlIChcKGFub255bW91c1wpKSAvVHJhcHBlZCAvRmFs
c2UKPj4KZW5kb2JqCjcgMCBvYmoKPDwKL0NvdW50IDEgL0tpZHMgWyA0IDAgUiBdIC9UeXBl
IC9QYWdlcwo+PgplbmRvYmoKOCAwIG9iago8PAovRmlsdGVyIFsgL0FTQ0lJODVEZWNvZGUg
L0ZsYXRlRGVjb2RlIF0gL0xlbmd0aCAxMzAxCj4+CnN0cmVhbQpHYXJuVzlpTChBJkFAWmNx
TlEidVtVWFMhJShRdT48Xyo0J0syUHBRZzwzVDU5PSUvRmdBL050SXNwMTRBL1UiSyhFQjdN
R2w/TGJbTSwrZ1BKbUsoOiVXZCleT0FkJ1I4XlxqalEja1dWNzNnKjpWTkdISUg5Q1hjN1w1
YmBqOGZxQXI3N3Qsc3MhPmU+UC4jaj8tRDBjVTNdQE1zODRWM1NwVm5OR2UxNWpaaUwrW3Jm
Kmw+TD1PMF9lLSMkUkYuUWBYcTFkOjleK0suSiJiIS0xci9tRmNRU09aWk4wKztkZ3JjZiNP
aGBGTFhUQmtROipoSWhebV4/JTctYWdydFpQRCMzKjYsPnVvSzFKQClESTkiMyMmQWQjNDRN
UEVzOmY1RjxhMkIvJj1rbWIxZGwiW2FpUT9rLCZeSjRaKi1uZTdlYW0jMlRaaGBqamJRa29e
YGdNMUNyJkxJOS4+M2pLS1RmU3JNWixNMylzSlZQcT9baFg2SWkwbTsiUmNIZUoiRSJha1BG
NDZlRWU9VidXKVxSbyRXVz4qSlpfT3BcVikyakEkL09VcDIqcXQ6dEchXjw9Szg7W1chXTtW
ZUZXXilAakwibEUzWyRCIlVxUEpmUklKcF9oV2ghcztOcUNEQ0h0IzE2I1JrV2VZQEpEYTtO
RmY9U0ImVk5bMmIpY15nPT81Lm01XnVBTmMsZlBsVDZVTjdhX3VFPE1iRkFZaGh0WDxoOmhi
PmRkVCVjcWdRNy5JVVlWQShyUipJbm5wRGQ+cShNbkJoTW9LQk1baWcpM15kVSRLKWc9YDcm
SW5eW2RcZVBLI1YoLCJVZF9OXDQxMUhGUzRHdUgyLUkwOUNqPDI8WW1eJiFXQnFacWpaXGxs
SClSS3NYXGVgLVwzQitnYkxrUiZPWFBzRlQoJkFoY206NjtBXWtVYmcqXUtBQFUuPVxdPFRw
cjFCc2VXSjlvRj8kMz9uTTxHXFdmSFdqZnQ3bEBGVjRdJ2RMWD4kIWs7MjU+SzosInJaLFkt
aTwzbGJeK15FZW1vQVZUVUhzRmdSSD1rL1hMWGxPISs3VG0nPio3JVBAK1FJXz5IKmVQXGhm
IVo5M0pvPlwyVkw7T1UxYDBzJmdxXEhyVExYXzVfUV8zLjBpazsjbVZiWVtDbjU2ZEIoT1pf
SlEmOzBWZltWcWMoVUtFOyhwL0BfaHRUOmJoSywmKENtdVUtLl1Zaz9tJCZGaWhfPEMhNVFw
TnEqNz4lODIpVUdaSSM9bEopVyttZ01QVzdFN2syYyImRU9YZmNpRjw+PlZvLWQ7V2ohPkhf
cGFUWTg4I1lnLz1UKU9CdUhgdXJCUChANzgwJSJLWDUiMkIqODA0TjNBQGEwP0ZoMDtILS9H
OlcrOyw0aVBrajtRWmlLRS4oTW9cXz0hNDoocDdLV2FRYGRdITVqYyFJQVckOj9PNVowPDdv
JzNtIztjJT9gNmhRJDBKPy4rdVIiMUdfJGlrdGdJIUpoZEFgM0psNkxUUG4qdW01W11sVFYp
Wkw8J1hFO1FjaTVcJzA6PG9WOWdWNFVqTDtXOGBvaUZPV1w8TWpsRVRpUUxpIlxBWFxmdT89
dWVBUlVqVldcM3BmO29KXUJaJlc1OmJYPFsydD4mSGxzTykzb1ZSVTxwIl8nUFVqbUEoPHNt
MCNTXi8qb1htJy5SXHFaJCdJIWltc0dDZTtbRCNYbG46QXRKUGc7Om9+PmVuZHN0cmVhbQpl
bmRvYmoKeHJlZgowIDkKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDczIDAwMDAwIG4g
CjAwMDAwMDAxMTQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMzMzIDAw
MDAwIG4gCjAwMDAwMDA1MjYgMDAwMDAgbiAKMDAwMDAwMDU5NCAwMDAwMCBuIAowMDAwMDAw
ODc3IDAwMDAwIG4gCjAwMDAwMDA5MzYgMDAwMDAgbiAKdHJhaWxlcgo8PAovSUQgCls8NjY5
M2E3ZTE3OTljMTIyYWE4OTIzZGI4ZDhlYzI5Y2M+PDY2OTNhN2UxNzk5YzEyMmFhODkyM2Ri
OGQ4ZWMyOWNjPl0KJSBSZXBvcnRMYWIgZ2VuZXJhdGVkIFBERiBkb2N1bWVudCAtLSBkaWdl
c3QgKGh0dHA6Ly93d3cucmVwb3J0bGFiLmNvbSkKCi9JbmZvIDYgMCBSCi9Sb290IDUgMCBS
Ci9TaXplIDkKPj4Kc3RhcnR4cmVmCjIzMjgKJSVFT0YK
--B_3856698945_1868877581--

View File

@ -0,0 +1,413 @@
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:05:00 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:04:59 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:04:59 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: =?Windows-1252?Q?(INVOICE)_Hyperbolic_Mortgage_=97_INV-2025-1187_(Due_Feb?=
=?Windows-1252?Q?_19)?=
Thread-Topic: =?Windows-1252?Q?(INVOICE)_Hyperbolic_Mortgage_=97_INV-2025-1187_(Due_Feb?=
=?Windows-1252?Q?_19)?=
Thread-Index: Ady2tkFaaGnbWjB6QMGI96nkpW927Q==
Date: Wed, 18 Mar 2026 17:04:59 +0800
Message-ID: <468ef2ff7c674dd88ec89e4f6e01b25d@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <468ef2ff7c674dd88ec89e4f6e01b25d@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: 7df358e5-3bcd-47c1-6b6f-08de84cd6f0b
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.4935283
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856698948_1508287491"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698948_1508287491
Content-type: multipart/alternative;
boundary="B_3856698948_97088826"
--B_3856698948_97088826
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: 7bit
Hello Finance Team,
Please find the attached invoice (INV-2025-1187) for services rendered to Summit Realty Partners by Hyperbolic Mortgage.
The total amount due is $6,114.98, which covers the following:
Loan Application Review Services
Automated Underwriting Analysis
Document Verification & Processing
Portfolio Risk Assessment Summary
PAYMENT INSTRUCTIONS:
Please remit the payment via ACH using the following internal banking details for Hyperbolic Mortgage:
Bank Account Number: 88291044
Routing Number: 126000247
If there are any discrepancies regarding this billing, please contact our Accounts Receivable department immediately at ar@hyperbolicmortgage.com.
Thank you,
Lukey
Accounts Receivable
Hyperbolic Mortgage
--B_3856698948_97088826
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<!--[if !mso]><style>v\:* {behavior:url(#default#VML);}
o\:* {behavior:url(#default#VML);}
w\:* {behavior:url(#default#VML);}
.shape {behavior:url(#default#VML);}
</style><![endif]--><style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
a:link, span.MsoHyperlink
{mso-style-priority:99;
color:#467886;
text-decoration:underline;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:846823218;
mso-list-template-ids:-647586680;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1
{mso-list-id:1498374779;
mso-list-template-ids:-1817390720;}
@list l1:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l1:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l1:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l1:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hello Finance Team,<span style=3D"mso-fareast-language:Z=
H-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Please find the attached invoice (<b>INV-2025-1187</b>=
) for services rendered to&nbsp;<b>Summit Realty Partners</b>&nbsp;by&nbsp;<=
b>Hyperbolic Mortgage</b>.<o:p></o:p></p>
<p class=3D"MsoNormal">The total amount due is&nbsp;<b>$6,114.98</b>, which c=
overs the following:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1">Loan Application Revi=
ew Services<o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 l=
fo1">Automated Underwriting Analysis<o:p></o:p></li><li class=3D"MsoNormal" st=
yle=3D"mso-list:l0 level1 lfo1">Document Verification &amp; Processing<o:p></o=
:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1">Portfolio Risk=
Assessment Summary<o:p></o:p></li></ul>
<p class=3D"MsoNormal"><b>PAYMENT INSTRUCTIONS:</b><br>
Please remit the payment via&nbsp;<b>ACH</b>&nbsp;using the following inter=
nal banking details for Hyperbolic Mortgage:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l1 level1 lfo2"><b>Bank Account Numbe=
r:</b>&nbsp;88291044<o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l1=
level1 lfo2"><b>Routing Number:</b>&nbsp;126000247<o:p></o:p></li></ul>
<p class=3D"MsoNormal">If there are any discrepancies regarding this billing,=
please contact our Accounts Receivable department immediately at
<a href=3D"mailto:ar@hyperbolicmortgage.com">ar@hyperbolicmortgage.com</a>.<s=
pan style=3D"mso-fareast-language:ZH-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Thank you,<o:p></o:p></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey</span><=
br>
Accounts Receivable<br>
<b>Hyperbolic Mortgage</b><o:p></o:p></p>
<div class=3D"MsoNormal" align=3D"center" style=3D"text-align:center">
<hr size=3D"1" width=3D"100%" align=3D"center">
</div>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856698948_97088826--
--B_3856698948_1508287491
Content-type: application/pdf; name="Invoice_INV-2025-1187_SummitRealty.pdf";
x-mac-creator="4F50494D";
x-mac-type="50444620"
Content-disposition: attachment;
filename="Invoice_INV-2025-1187_SummitRealty.pdf"
Content-transfer-encoding: base64
JVBERi0xLjQKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUgo+Pgpl
bmRvYmoKMiAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYSAvRW5jb2RpbmcgL1dpbkFu
c2lFbmNvZGluZyAvTmFtZSAvRjEgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250Cj4+CmVu
ZG9iagozIDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLUJvbGQgL0VuY29kaW5nIC9X
aW5BbnNpRW5jb2RpbmcgL05hbWUgL0YyIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+
PgplbmRvYmoKNCAwIG9iago8PAovQ29udGVudHMgOCAwIFIgL01lZGlhQm94IFsgMCAwIDYx
MiA3OTIgXSAvUGFyZW50IDcgMCBSIC9SZXNvdXJjZXMgPDwKL0ZvbnQgMSAwIFIgL1Byb2NT
ZXQgWyAvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJIF0KPj4gL1JvdGF0ZSAw
IC9UcmFucyA8PAoKPj4gCiAgL1R5cGUgL1BhZ2UKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL1Bh
Z2VNb2RlIC9Vc2VOb25lIC9QYWdlcyA3IDAgUiAvVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoK
NiAwIG9iago8PAovQXV0aG9yIChcKGFub255bW91c1wpKSAvQ3JlYXRpb25EYXRlIChEOjIw
MjUxMTIxMDIxNDI3KzAwJzAwJykgL0NyZWF0b3IgKFwodW5zcGVjaWZpZWRcKSkgL0tleXdv
cmRzICgpIC9Nb2REYXRlIChEOjIwMjUxMTIxMDIxNDI3KzAwJzAwJykgL1Byb2R1Y2VyIChS
ZXBvcnRMYWIgUERGIExpYnJhcnkgLSB3d3cucmVwb3J0bGFiLmNvbSkgCiAgL1N1YmplY3Qg
KFwodW5zcGVjaWZpZWRcKSkgL1RpdGxlIChcKGFub255bW91c1wpKSAvVHJhcHBlZCAvRmFs
c2UKPj4KZW5kb2JqCjcgMCBvYmoKPDwKL0NvdW50IDEgL0tpZHMgWyA0IDAgUiBdIC9UeXBl
IC9QYWdlcwo+PgplbmRvYmoKOCAwIG9iago8PAovRmlsdGVyIFsgL0FTQ0lJODVEZWNvZGUg
L0ZsYXRlRGVjb2RlIF0gL0xlbmd0aCAxMjg5Cj4+CnN0cmVhbQpHYXNJZ2FgPyxxJkFAQltc
Z2tqZVM+JiJIYCY0R0BHISdhb01lcGVfYD49PmJDclJfS0s6az5xWUZjIlhEJ1clYGFEL0Eq
NHU/VmpLN0pYdDtaOTA4XnJJI2NeKjVmaktJXDFSQmdfWTE1KSNidSZaTG5hJXJPV0VOJklg
RGFpPiIpT1ohaktVX25kZW5UPk88RmM9KnAiNCtzOm4sWGxmaGdiWDheQSVpJ1txZFVfPVY9
Nm8vWCxJXjRuOkI3SW1gdUBjTSY/QF5eWWAmOFt0b01FRmNJX15TNW5lTSJqcWorc2lHOVJB
SDwvb1Q+NmZBOSNRSEhdXXFrSHNgJGJvMitFbko6dTdKN0BlS2RJJzZmKiotW1AhITEtQ0w7
dGhxPSdNTkNxPXBGMUU+bkpdSTU0OiM5UGJnR0pObmZbXkkjTzZsX1gnNzY3U09YZVJgNVxQ
M0Q3ajtrIkk4QEFfb25GYmBpYi9pQDVVOmtcXWZILCNVJkMlVWhGN0h0MmA4LnArNEFcTUBQ
aWAvPFJfW11DTTlQKUJkOSMkaCcraGE0YyJBP1RPMEZ1JkZ1NzdCImlaOWtLMDJcUFkqX2o4
RiQsLCVRdChYSCp1U2lQIi5wKWUnMCM1XSlOUzEwX0IiK0pLa1FjJzRAMy1MLydHIyQhPCZz
dV00SGw4Pjlva11FIW5KKlFBKz83MCpedSUwJ0tCMDdfRz5MNjFQWjc0bGE3IWBvImpiWkdg
TytjaU43VFEpNnJQOmI4OD5RYlJ0OnFdWCkvQm9DT3BbMixyKGBpXEcpMyFgVEAmJFwnKjk+
PyJVKF1JZz5tajpMbkU/Oi9KdCxSUD1pTG9icTBwZGk2MmtcciQ1NS9AYUlYVm5AZEwlOD5N
b0BNJTZcZmxHPkZnN1pJKFotNkFRLVckOi9kclRfMF9tZktZMCNGWVcnSWxiXD82QSojNEUx
KkdAaGpubFFqSydwbz0wa1lQYV41OGxPYSU8b2JvWyxvKGokbSMiZjxNKk1RPCxRJ1NfU2Qn
WDxJXFZzVGFvKUQkJkE9MyQwW2tYOUczWFRtJ2tRZHRUKVVwYGNya3VOOicjVVVGQCFrNmRD
bFVZZUwjIW8pZ0dqXDs9ZTZsIkVdOlNLQEBKN1xIa2g4QkNTbWlkJVNDPUVXT3AoYnI7QFwp
Tl5YImMoISJxXElyR14jUytfcjAlMEY5Ti80cyxWM2plNj5XbDVXOU80SDUlQmwmZjhnW00p
RjojNE5YMmlTOiQ7ZTxrOWIoYm5oSSMsTUYmIyE8SG9fKzVCXkc+KGlEN2hsR1VKbmQ/QXUh
L1JCIXVTIjZTaidkLzYvUUxFXVBucE5vPGA7LG90NyRPU3QqbmI7WiEkcExiTUxbTVA8Xmhz
JSddInVySlVda1RuQipQMk5sX2MlVD1FNSE+XVNbMy9eaEo4Nig9W11oNm8/T1UjOkg2Tlgl
TTFlQ2Y8czNXQWheamwlJnRCRkkwMTJPJDIzaDZHZXRFaEkyV1kpQ1VwaDBBanRDYmhJKmN0
YkhlKTYqREZEbm9hOC0xIURkT0xpTUY8algnJFgzN2pdPWJVTyVmPUxvVmJeIUR0MFhNXEw5
V0MxKyVqUDZtRXBTbSYiKjoiNS0tZUQuKyMnKyZBJVNrO05xbi0yWGBab3NRPiVBY1lrYDVI
YC1ZdU4uRDxQbCZXbVlDYyVRaWA2RyNESUgvNTZ+PmVuZHN0cmVhbQplbmRvYmoKeHJlZgow
IDkKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDczIDAwMDAwIG4gCjAwMDAwMDAxMTQg
MDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMzMzIDAwMDAwIG4gCjAwMDAw
MDA1MjYgMDAwMDAgbiAKMDAwMDAwMDU5NCAwMDAwMCBuIAowMDAwMDAwODc3IDAwMDAwIG4g
CjAwMDAwMDA5MzYgMDAwMDAgbiAKdHJhaWxlcgo8PAovSUQgCls8MjY4OGQyNjQ1MDM5MTY4
NjMzNWFjZmViNjg0YzBmZWQ+PDI2ODhkMjY0NTAzOTE2ODYzMzVhY2ZlYjY4NGMwZmVkPl0K
JSBSZXBvcnRMYWIgZ2VuZXJhdGVkIFBERiBkb2N1bWVudCAtLSBkaWdlc3QgKGh0dHA6Ly93
d3cucmVwb3J0bGFiLmNvbSkKCi9JbmZvIDYgMCBSCi9Sb290IDUgMCBSCi9TaXplIDkKPj4K
c3RhcnR4cmVmCjIzMTYKJSVFT0YK
--B_3856698948_1508287491--

View File

@ -0,0 +1,311 @@
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 16:58:09 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 16:58:09 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 16:58:09 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: =?Windows-1252?Q?(URGENT/CONFIDENTIAL)_Hyperbolic_Mortgage_=97_Preliminar?=
=?Windows-1252?Q?y_CFPB_Compliance_Draft_2026?=
Thread-Topic: =?Windows-1252?Q?(URGENT/CONFIDENTIAL)_Hyperbolic_Mortgage_=97_Preliminar?=
=?Windows-1252?Q?y_CFPB_Compliance_Draft_2026?=
Thread-Index: Ady2tRNOGsS7F2qBSyuNM6QQscsmPg==
Date: Wed, 18 Mar 2026 16:58:09 +0800
Message-ID: <bb7086eb31ea4752965a5276472fec7e@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <bb7086eb31ea4752965a5276472fec7e@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: b734f259-80e6-4ec5-7dac-08de84cc7a74
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.4750632
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856698956_82075933"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698956_82075933
Content-type: multipart/alternative;
boundary="B_3856698956_3713518528"
--B_3856698956_3713518528
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Hello,
=20
Attached is the Internal Draft of the Compliance Report on Mortgage Servici=
ng Practices for Hyperbolic Mortgage, dated February 10, 2025 (p. 1).
This document is a CONFIDENTIAL =E2=80=94 DRAFT REGULATORY DOCUMENT intended for =
the Consumer Financial Protection Bureau (CFPB) and is not for public releas=
e (p. 1). It contains sensitive internal data, including:
Overview of servicing workflow updates
Early intervention procedures for delinquent borrowers
Loss-mitigation evaluation criteria
Draft audit results for payment processing accuracy (p. 1)
Please review the Outstanding Items section, specifically the validation of=
escrow analysis procedures and the internal audit of vendor servicing syste=
ms, before we finalize the regulatory submission (p. 1).
As this is marked Confidential =E2=80=94 Draft Only, ensure this remains within a=
uthorized channels.
=20
Best regards,
Lukey
Legal & Compliance Team
Hyperbolic Mortgage
--B_3856698956_3713518528
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:1768621719;
mso-list-template-ids:-298053842;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hello,<o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
<p class=3D"MsoNormal">Attached is the&nbsp;<b>Internal Draft</b>&nbsp;of the=
&nbsp;<b>Compliance Report on Mortgage Servicing Practices</b>&nbsp;for&nbsp=
;<b>Hyperbolic Mortgage</b>, dated February 10, 2025 (p. 1).<o:p></o:p></p>
<p class=3D"MsoNormal">This document is a&nbsp;<b>CONFIDENTIAL =E2=80=94 DRAFT REGU=
LATORY DOCUMENT</b>&nbsp;intended for the&nbsp;<b>Consumer Financial Protect=
ion Bureau (CFPB)</b>&nbsp;and is not for public release (p. 1). It contains=
sensitive internal data, including:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>Overview of servic=
ing workflow updates</b><o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-lis=
t:l0 level1 lfo1"><b>Early intervention procedures for delinquent borrowers<=
/b><o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>=
Loss-mitigation evaluation criteria</b><o:p></o:p></li><li class=3D"MsoNormal"=
style=3D"mso-list:l0 level1 lfo1"><b>Draft audit results for payment processi=
ng accuracy</b>&nbsp;(p. 1)<o:p></o:p></li></ul>
<p class=3D"MsoNormal">Please review the&nbsp;<b>Outstanding Items</b>&nbsp;s=
ection, specifically the&nbsp;<b>validation of escrow analysis procedures</b=
>&nbsp;and the&nbsp;<b>internal audit of vendor servicing systems</b>, befor=
e we finalize the regulatory submission (p. 1).<o:p></o:p></p>
<p class=3D"MsoNormal">As this is marked&nbsp;<b>Confidential =E2=80=94 Draft Only<=
/b>, ensure this remains within authorized channels.<o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
<p class=3D"MsoNormal">Best regards,<o:p></o:p></p>
<p class=3D"MsoNormal">Lukey<br>
Legal &amp; Compliance Team<br>
<b>Hyperbolic Mortgage</b><o:p></o:p></p>
</div>
</body>
</html>
--B_3856698956_3713518528--
--B_3856698956_82075933
Content-type: application/pdf; name="Regulatory Internal File.pdf";
x-mac-creator="4F50494D";
x-mac-type="50444620"
Content-disposition: attachment;
filename="Regulatory Internal File.pdf"
Content-transfer-encoding: base64
JVBERi0xLjQKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUgo+Pgpl
bmRvYmoKMiAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYSAvRW5jb2RpbmcgL1dpbkFu
c2lFbmNvZGluZyAvTmFtZSAvRjEgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250Cj4+CmVu
ZG9iagozIDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLUJvbGQgL0VuY29kaW5nIC9X
aW5BbnNpRW5jb2RpbmcgL05hbWUgL0YyIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+
PgplbmRvYmoKNCAwIG9iago8PAovQ29udGVudHMgOCAwIFIgL01lZGlhQm94IFsgMCAwIDYx
MiA3OTIgXSAvUGFyZW50IDcgMCBSIC9SZXNvdXJjZXMgPDwKL0ZvbnQgMSAwIFIgL1Byb2NT
ZXQgWyAvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJIF0KPj4gL1JvdGF0ZSAw
IC9UcmFucyA8PAoKPj4gCiAgL1R5cGUgL1BhZ2UKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL1Bh
Z2VNb2RlIC9Vc2VOb25lIC9QYWdlcyA3IDAgUiAvVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoK
NiAwIG9iago8PAovQXV0aG9yIChcKGFub255bW91c1wpKSAvQ3JlYXRpb25EYXRlIChEOjIw
MjUxMTIxMDI0MDM3KzAwJzAwJykgL0NyZWF0b3IgKFwodW5zcGVjaWZpZWRcKSkgL0tleXdv
cmRzICgpIC9Nb2REYXRlIChEOjIwMjUxMTIxMDI0MDM3KzAwJzAwJykgL1Byb2R1Y2VyIChS
ZXBvcnRMYWIgUERGIExpYnJhcnkgLSB3d3cucmVwb3J0bGFiLmNvbSkgCiAgL1N1YmplY3Qg
KFwodW5zcGVjaWZpZWRcKSkgL1RpdGxlIChcKGFub255bW91c1wpKSAvVHJhcHBlZCAvRmFs
c2UKPj4KZW5kb2JqCjcgMCBvYmoKPDwKL0NvdW50IDEgL0tpZHMgWyA0IDAgUiBdIC9UeXBl
IC9QYWdlcwo+PgplbmRvYmoKOCAwIG9iago8PAovRmlsdGVyIFsgL0FTQ0lJODVEZWNvZGUg
L0ZsYXRlRGVjb2RlIF0gL0xlbmd0aCAxMDc1Cj4+CnN0cmVhbQpHYXJuVjlsbyNCJkFANy5t
Jjw/NVdKZDtYODhoJypENUsuY2AnJjNzTV8kTUxNKERNUE9FI2J1PSMpW3VPQzxrRVVPRSRy
Z1VBQVw1LyNEPmM3PUcyKidURV44Mko9J09JZGIyT0FGZG01UGZHY0RHQ1tyRSVxV1JrdWc1
NzFXcSFtOTA4czkhcU00VjEuWzAkLUhjSjNIKCtDJihcRGNUNkxTTjQ1SkEpbWhXSylhJzo8
RmMhKE87cUU9VD4wWUoyRGNxL2w/LSkiTT1cNUpoLSM+Um8oPUFZXE03WTktXklocyhyRjY7
LC9STEdqJWxRSWx0IylQLzpCUVlCQWFiXlwoLEorZE1mSmM0SFNlJ2hhI2wkay5LckxiSHIk
RE84RDp0NGJXN1tqMl1RJVBTREk+SnI8Sis2Vk9tWl4pNlIiVVQxJGVIZXRpUjFCXVsmTiQz
KFI7NCpCRWUiYV1scFgsInMvLihGTm1XK1pUKDEwVEwlOD5KJDZiN2dbW2ZhOTldakRLam9v
SUNBcVZKNmdkOjFDTiM7W2w/NHQ0XDM+bE0/dVgnblE+Zy1FN0EmTSQhJ2E7SEMqYSZCLE5R
cUc2VD89USVRSDVgX0tcUShlLEJZYXJUcTBJaCgnamtTciopRCJRQ2R0N0grU2BhNDs4IzFM
KmQqSkU0L3RKNERHM18xYyZqMz9wJTZnQGxMXWZBUzlwbVdUL11ITU5jLytQTmZcWitdaisk
QUI2ck9zcmk+USw3UkAvUWxxQVc9bnVsUlU7PWpNWnAjUDVRXXJuUVcsK0JcMGheRllGJy1p
Z2ZrJSUodDcxLUEhZ0AyYHA1dEokcyQlTSRYVClUQ1AuMG0nUWxNVUxOS2leYUBXMjRvakNE
SF9hNURcZj87UFVEI3Rwa3BYOzVRRSgxUHVAOExUKk04NmNOZFJPLm9DY0QzbUBrW1ZRLHVM
bmdIW15GYUcjIiltI2pqb2prTlFAU1huU0opP290cydnOlN1NElWbXAsM10wQDhiZklAPVsz
ZlpVYlpAOFQvRTZgSThzUFZYb3VAPHAkPmtgTi0sK15LJzVkLTc4aVgpN2BXMzAwb1pdbUQi
QDNGKD00KFxqXihhdW9laz1VTz5IcDZHI2EjM0AyW05sXkhcdStCTFRtN1k6OnUwQUNOQCFo
PFZpXkc2N1IxK0ZYWENMKyVQPyNJSTgoVVFCbGtrMXFLYCMoOixfW0ljIzpqZTNdUCZVRF9r
VS9FOzpjPEkjbThAdSZSV0lJazxVLDpPTnJWTVUlR15kLSliQ1dbXV5bQUNSaToxXlVtLTVA
UD1QTFw7WChjOVgnZTgzJDFDRSQ2ZUIxTEhMTCk6SW9vP1MnNDFmXmhWP3VyPCNQbVQjVERy
KFAwQy5SNm9DWTxtX0VtakJyaylUWSpyVysxOTFAIn4+ZW5kc3RyZWFtCmVuZG9iagp4cmVm
CjAgOQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwNzMgMDAwMDAgbiAKMDAwMDAwMDEx
NCAwMDAwMCBuIAowMDAwMDAwMjIxIDAwMDAwIG4gCjAwMDAwMDAzMzMgMDAwMDAgbiAKMDAw
MDAwMDUyNiAwMDAwMCBuIAowMDAwMDAwNTk0IDAwMDAwIG4gCjAwMDAwMDA4NzcgMDAwMDAg
biAKMDAwMDAwMDkzNiAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9JRCAKWzw4ZTllMDdkNTRjZDRi
ZDkxZTg0OTBlZTUzODg2YjRlYT48OGU5ZTA3ZDU0Y2Q0YmQ5MWU4NDkwZWU1Mzg4NmI0ZWE+
XQolIFJlcG9ydExhYiBnZW5lcmF0ZWQgUERGIGRvY3VtZW50IC0tIGRpZ2VzdCAoaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tKQoKL0luZm8gNiAwIFIKL1Jvb3QgNSAwIFIKL1NpemUgOQo+
PgpzdGFydHhyZWYKMjEwMgolJUVPRgo=
--B_3856698956_82075933--

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,334 @@
Received: from BWNEWEXCH02.boardware.com.mo (172.16.21.3) by
BWNEWEXCH01.boardware.com.mo (172.16.21.2) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1544.4
via Mailbox Transport; Wed, 18 Mar 2026 17:03:42 +0800
Received: from BWNEWEXCH01.boardware.com.mo (172.16.21.2) by
BWNEWEXCH02.boardware.com.mo (172.16.21.3) with Microsoft SMTP Server
(version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.2.1544.4; Wed, 18 Mar 2026 17:03:40 +0800
Received: from BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72]) by
BWNEWEXCH01.boardware.com.mo ([fe80::50d4:5d28:928f:9c72%16]) with mapi id
15.02.1544.004; Wed, 18 Mar 2026 17:03:42 +0800
From: "Lukey KW Leong (Boardware)" <lukey.kw.leong@boardware.com>
To: "Ricky IW Chan (Boardware)" <ricky.iw.chan@boardware.com>
Subject: =?Windows-1252?Q?(CONFIDENTIAL)_Payroll_Record_=97_Daniel_Reyes_(HM-55291?=
=?Windows-1252?Q?)?=
Thread-Topic: =?Windows-1252?Q?(CONFIDENTIAL)_Payroll_Record_=97_Daniel_Reyes_(HM-55291?=
=?Windows-1252?Q?)?=
Thread-Index: Ady2tgj/fw/qtDLUS2W5CNdvIbm5qw==
Date: Wed, 18 Mar 2026 17:03:41 +0800
Message-ID: <ffb49a825516420885bdf3364bdc8da5@boardware.com>
Accept-Language: en-US, zh-CN
Content-Language: en-US
X-MS-Has-Attach: yes
X-MS-Exchange-Organization-SCL: -1
X-MS-TNEF-Correlator: <ffb49a825516420885bdf3364bdc8da5@boardware.com>
MIME-Version: 1.0
X-MS-Exchange-Organization-MessageDirectionality: Originating
X-MS-Exchange-Organization-AuthSource: BWNEWEXCH01.boardware.com.mo
X-MS-Exchange-Organization-AuthAs: Internal
X-MS-Exchange-Organization-AuthMechanism: 04
X-Originating-IP: [10.130.2.6]
X-MS-Exchange-Organization-Network-Message-Id: 7b9129fe-7df6-4624-9f4a-08de84cd40ab
Return-Path: lukey.kw.leong@boardware.com
X-MS-Exchange-Transport-EndToEndLatency: 00:00:00.3996192
X-MS-Exchange-Processed-By-BccFoldering: 15.02.1544.004
Content-type: multipart/mixed;
boundary="B_3856698950_3788612792"
> This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
--B_3856698950_3788612792
Content-type: multipart/alternative;
boundary="B_3856698950_1611823738"
--B_3856698950_1611823738
Content-type: text/plain;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
Hi,
=20
I am forwarding the Payroll Statement for employee Daniel Reyes (Employee I=
D: HM-55291) for the pay period of January 1=E2=80=9315, 2025 (p. 1).
This Compensation Record contains sensitive information including:
Net Pay: $2,360.96 (Gross: $3,358.00) (p. 1).
Tax Deductions: Federal Tax ($412.00) and State Tax ($128.00) (p. 1).
Banking Details: Direct Deposit to Evergreen Credit Union (Account ending i=
n **** 4429) (p. 1).
Employer Info: Hyperbolic Mortgage, Inc., Seattle, WA (p. 1).
This document is marked as "Confidential Payroll Information =E2=80=94 For Employ=
ee Use Only. Do Not Distribute." (p. 2). Please handle this with the highest=
level of security.
=20
Regards,
Lukey
Loan Processing Department (p. 1)
Hyperbolic Mortgage
=20
--B_3856698950_1611823738
Content-type: text/html;
charset="UTF-8"
Content-transfer-encoding: quoted-printable
<html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsof=
t-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:office:word" xmlns:m=
=3D"http://schemas.microsoft.com/office/2004/12/omml" xmlns=3D"http://www.w3.org=
/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
{font-family:Wingdings;
panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
{font-family:PMingLiU;
panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
{font-family:"Cambria Math";
panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
{font-family:DengXian;
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:Aptos;}
@font-face
{font-family:"\@DengXian";
panose-1:2 1 6 0 3 1 1 1 1 1;}
@font-face
{font-family:"\@PMingLiU";
panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
{margin:0in;
font-size:12.0pt;
font-family:"Aptos",sans-serif;
mso-ligatures:standardcontextual;}
span.EmailStyle17
{mso-style-type:personal-compose;
font-family:"Aptos",sans-serif;
color:windowtext;}
.MsoChpDefault
{mso-style-type:export-only;}
@page WordSection1
{size:8.5in 11.0in;
margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
{page:WordSection1;}
/* List Definitions */
@list l0
{mso-list-id:1602491166;
mso-list-template-ids:1269587156;}
@list l0:level1
{mso-level-number-format:bullet;
mso-level-text:\F0B7;
mso-level-tab-stop:.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Symbol;}
@list l0:level2
{mso-level-number-format:bullet;
mso-level-text:o;
mso-level-tab-stop:1.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:"Courier New";
mso-bidi-font-family:"Times New Roman";}
@list l0:level3
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:1.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level4
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level5
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:2.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level6
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level7
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:3.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level8
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.0in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
@list l0:level9
{mso-level-number-format:bullet;
mso-level-text:\F0A7;
mso-level-tab-stop:4.5in;
mso-level-number-position:left;
text-indent:-.25in;
mso-ansi-font-size:10.0pt;
font-family:Wingdings;}
ol
{margin-bottom:0in;}
ul
{margin-bottom:0in;}
--></style><!--[if gte mso 9]><xml>
<o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" />
</xml><![endif]--><!--[if gte mso 9]><xml>
<o:shapelayout v:ext=3D"edit">
<o:idmap v:ext=3D"edit" data=3D"1" />
</o:shapelayout></xml><![endif]-->
</head>
<body lang=3D"EN-US" link=3D"#467886" vlink=3D"#96607D" style=3D"word-wrap:break-wo=
rd">
<div class=3D"WordSection1">
<p class=3D"MsoNormal">Hi,<span style=3D"mso-fareast-language:ZH-TW"><o:p></o:p=
></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">I am forwarding the&nbsp;<b>Payroll Statement</b>&nbsp=
;for employee&nbsp;<b>Daniel Reyes</b>&nbsp;(<b>Employee ID: HM-55291</b>) f=
or the pay period of January 1=E2=80=9315, 2025 (p. 1).<o:p></o:p></p>
<p class=3D"MsoNormal">This&nbsp;<b>Compensation Record</b>&nbsp;contains sen=
sitive information including:<o:p></o:p></p>
<ul style=3D"margin-top:0in" type=3D"disc">
<li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>Net Pay:</b>&nbsp;=
$2,360.96 (Gross: $3,358.00) (p. 1).<o:p></o:p></li><li class=3D"MsoNormal" st=
yle=3D"mso-list:l0 level1 lfo1"><b>Tax Deductions:</b>&nbsp;Federal Tax ($412.=
00) and State Tax ($128.00) (p. 1).<o:p></o:p></li><li class=3D"MsoNormal" sty=
le=3D"mso-list:l0 level1 lfo1"><b>Banking Details:</b>&nbsp;Direct Deposit to&=
nbsp;<b>Evergreen Credit Union</b>&nbsp;(Account ending in **** 4429) (p. 1)=
.<o:p></o:p></li><li class=3D"MsoNormal" style=3D"mso-list:l0 level1 lfo1"><b>Em=
ployer Info:</b>&nbsp;Hyperbolic Mortgage, Inc., Seattle, WA (p. 1).<o:p></o=
:p></li></ul>
<p class=3D"MsoNormal">This document is marked as&nbsp;<b>&quot;Confidential =
Payroll Information =E2=80=94 For Employee Use Only. Do Not Distribute.&quot;</b>&=
nbsp;(p. 2). Please handle this with the highest level of security.<span sty=
le=3D"mso-fareast-language:ZH-TW"><o:p></o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW"><o:p>&nbsp;</=
o:p></span></p>
<p class=3D"MsoNormal">Regards,<span style=3D"mso-fareast-language:ZH-TW"><o:p>=
</o:p></span></p>
<p class=3D"MsoNormal"><span style=3D"mso-fareast-language:ZH-TW">Lukey<br>
</span>Loan Processing Department (p. 1)<br>
<b>Hyperbolic Mortgage</b><o:p></o:p></p>
<p class=3D"MsoNormal"><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
--B_3856698950_1611823738--
--B_3856698950_3788612792
Content-type: application/pdf; name="Daniel_Reyes_Payroll_Jan2025.pdf";
x-mac-creator="4F50494D";
x-mac-type="50444620"
Content-disposition: attachment;
filename="Daniel_Reyes_Payroll_Jan2025.pdf"
Content-transfer-encoding: base64
JVBERi0xLjQKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgaHR0cDov
L3d3dy5yZXBvcnRsYWIuY29tCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUgo+Pgpl
bmRvYmoKMiAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYSAvRW5jb2RpbmcgL1dpbkFu
c2lFbmNvZGluZyAvTmFtZSAvRjEgL1N1YnR5cGUgL1R5cGUxIC9UeXBlIC9Gb250Cj4+CmVu
ZG9iagozIDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhLUJvbGQgL0VuY29kaW5nIC9X
aW5BbnNpRW5jb2RpbmcgL05hbWUgL0YyIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+
PgplbmRvYmoKNCAwIG9iago8PAovQ29udGVudHMgOSAwIFIgL01lZGlhQm94IFsgMCAwIDYx
MiA3OTIgXSAvUGFyZW50IDggMCBSIC9SZXNvdXJjZXMgPDwKL0ZvbnQgMSAwIFIgL1Byb2NT
ZXQgWyAvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJIF0KPj4gL1JvdGF0ZSAw
IC9UcmFucyA8PAoKPj4gCiAgL1R5cGUgL1BhZ2UKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0Nv
bnRlbnRzIDEwIDAgUiAvTWVkaWFCb3ggWyAwIDAgNjEyIDc5MiBdIC9QYXJlbnQgOCAwIFIg
L1Jlc291cmNlcyA8PAovRm9udCAxIDAgUiAvUHJvY1NldCBbIC9QREYgL1RleHQgL0ltYWdl
QiAvSW1hZ2VDIC9JbWFnZUkgXQo+PiAvUm90YXRlIDAgL1RyYW5zIDw8Cgo+PiAKICAvVHlw
ZSAvUGFnZQo+PgplbmRvYmoKNiAwIG9iago8PAovUGFnZU1vZGUgL1VzZU5vbmUgL1BhZ2Vz
IDggMCBSIC9UeXBlIC9DYXRhbG9nCj4+CmVuZG9iago3IDAgb2JqCjw8Ci9BdXRob3IgKFwo
YW5vbnltb3VzXCkpIC9DcmVhdGlvbkRhdGUgKEQ6MjAyNTExMjEwMjM4MTcrMDAnMDAnKSAv
Q3JlYXRvciAoXCh1bnNwZWNpZmllZFwpKSAvS2V5d29yZHMgKCkgL01vZERhdGUgKEQ6MjAy
NTExMjEwMjM4MTcrMDAnMDAnKSAvUHJvZHVjZXIgKFJlcG9ydExhYiBQREYgTGlicmFyeSAt
IHd3dy5yZXBvcnRsYWIuY29tKSAKICAvU3ViamVjdCAoXCh1bnNwZWNpZmllZFwpKSAvVGl0
bGUgKFwoYW5vbnltb3VzXCkpIC9UcmFwcGVkIC9GYWxzZQo+PgplbmRvYmoKOCAwIG9iago8
PAovQ291bnQgMiAvS2lkcyBbIDQgMCBSIDUgMCBSIF0gL1R5cGUgL1BhZ2VzCj4+CmVuZG9i
ago5IDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxhdGVEZWNvZGUgXSAv
TGVuZ3RoIDEzODYKPj4Kc3RyZWFtCkdhdG07YWA/LHEmQUBya3FPaUoyWjVFXUhcQklbVWpP
Ym8iWidcLls4ZiprSCd1aktAJj09P11mMiUkZyg2WXNSMVNwZzoyLiY5YEpGODEuIT80OCFy
L09bKlpPZjwsJjBNXCgtUXBXQiY3MzcqNFNuWiplNEBtamYvJGdBZnFoRUZObU5jbU9mL1hy
TUlAbkVoaDddUSJDXElQIVk4WG45LFFQXl4tQ0NZP2MsIXJLWDcwJEQkVU1BNFpMM2ddc3NO
amNgPjFcMG5fYjdxdT9jJiFeRVheL21odF1ISF9SNEp0JCQwUHV1LTRHPWlrNEBfUSVpT24n
PWRtKl1MaWQsKSpMY0RvIlFiZVtlJCVdIVdBY0dWVXNDJCNmYD1vKzxCWT5XNj44RUNwPkxO
OzNDZkdMSjBNZSlyI25sO2kkMHRyR2cqVHJoXSg8N2BObkQtTGciTTorRzZYQVE9RV9aX2Jr
O1xfWzZLcUhdXSdGUjJNW10jUiptKyg5YztvQV1wWWxxVVYyRmtVZkRXJXMkRENkV2k1Vzlb
VCljOEoqc3NiPFxuO1ZEJkNfY3BUJnJtWXBkOWohZ0M4Lm9WXzlmMCRhWS9oUlNnNVJuLkVt
K0YnVW86IVI5MChqNHJuZVwtbjhHNClYbERBIjpANio+OkokRSshMihnNmJ1Ky1qYFprRzEo
ZHBMRlBsN3BvT1s+Xz1MPiVORnRSaFxHR008OTZYZ19EaDQ3I1RhIj9DRCxDaFJkLz4pQltA
ZypbKFJhLTZEJjBPTy9lPXVXRWBGQU9SMiRlJEBQaVduRThNXiVxOThEI0ssUWMiWjNRSk9r
PzIrUXIucHFgZixiKFRlcl5TaHBJY3RWVnQoQnBjVS1IZW5hJUNDKURrcSYwTU5XaGFEJGJe
KG5RaXRlPltYS00zaypWaFZfTVJySVopKmQodVFbMyZYPU1oI2VYQiNzLTg5X1dWWWpeZi5D
Nk9bWlFWVyNpN1lqWiducjQ8NEQqbWY6bSNHcyo9YlwpNCZeNU5ERy5hUG8pYT5WKygzcW5q
ZS8iYi9CZVJbLXMyJWdnSFE+QFpCcENkRyhmKkMjVjkyTEsxU0dXOFlcSHRbPlY+TUpZQFoo
WFdkMkxNSD5gMzBBInBOQCpPTyVkI1FHJCY8NHBZLEQ9WChAN2BCYzQpS108bU9UVSY9ZHM3
UyZlRDtuNEklUE8qZnQyOyRlYFRxRyM4JGI7S28sUzVqSkxXP0MhLD1XMG5AUCpGWE5QXXMj
P0IjcDozXDFCODpzSlNbN1k1XCEnZTdkJXAkSFhNTkhobk5oKUFtMzdQPF5sVnVfVDJpPk1g
OyFtO1lzTilrRnE5YnVkUGBBJkYuUjkmaWZANWAhX0ZKQmUuNC1vYnVESG9lcmtUPWMvWFVB
XS4iOGBMcGBeUDJdKk5KI3VxXyxwK1AuN1lNbEZwRWgpcEVLPURnTiwvWF1qSXRYYVFCJ3Ml
OjI7JDhkLVYuXDpzODpWVE4rbEZbInBtaUxtdTVIMCVEMkZnV1JgdTZyYyVXP0U7WTxORDlO
SHI3O3FdPkpTbjE6KCUzVCFIYjVAZVZFSlUkVEVHalloKSpqWSV0RzIoOUk/YkROdDdway43
TktvR1oxNEk/c1owbCtFLUYyZztoW0VfSFEnKm5URituMihpRzcjL2JGXF9aTU4ialcvNmNS
cmY4Wy1bVCFzLFZeSWEmOj4/L1ByVCQ3YzNUIlg6W11EZTp1STszQlheNC9zLD1OSFdUMSlw
c2IsQ0dcalNWVW8wbTZwanM6MS5ESFcxXnQtUUFrISlCVXRnbltnPDEkWiRSaSk4LGlXaCsl
S2Z+PmVuZHN0cmVhbQplbmRvYmoKMTAgMCBvYmoKPDwKL0ZpbHRlciBbIC9BU0NJSTg1RGVj
b2RlIC9GbGF0ZURlY29kZSBdIC9MZW5ndGggMjE2Cj4+CnN0cmVhbQpHYXInYjBha2lQJ0Vt
PldeWiVfKlcoZVxCWTFhV287JSVBYldMP0YpYypNKyhxWDcuOjM2OG9UTm9ULz5gJCdOPlto
ZFVlLzUkSSorPDBlcThnKmpWLGpRPEkwIWM7QEZWO11xcUozRVpDY2BELlNVN1Nrblhnb0Ey
VjgnblNucC4oWlJzaiJoaipsRzJFOTY+LEBkc2ElQ3JxQFJPZydYQTE1UW5CTz9KdVYvWWk7
Tm4rVU5FbCpBaUVudSk/WUFeVj0zVExpbzBuKkFdSjouRT0qXVc5fj5lbmRzdHJlYW0KZW5k
b2JqCnhyZWYKMCAxMQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwNzMgMDAwMDAgbiAK
MDAwMDAwMDExNCAwMDAwMCBuIAowMDAwMDAwMjIxIDAwMDAwIG4gCjAwMDAwMDAzMzMgMDAw
MDAgbiAKMDAwMDAwMDUyNiAwMDAwMCBuIAowMDAwMDAwNzIwIDAwMDAwIG4gCjAwMDAwMDA3
ODggMDAwMDAgbiAKMDAwMDAwMTA3MSAwMDAwMCBuIAowMDAwMDAxMTM2IDAwMDAwIG4gCjAw
MDAwMDI2MTMgMDAwMDAgbiAKdHJhaWxlcgo8PAovSUQgCls8YzNkM2Q3MDA0MTgzM2I0MzE5
Y2VhOTIwMmY1MzVhZDE+PGMzZDNkNzAwNDE4MzNiNDMxOWNlYTkyMDJmNTM1YWQxPl0KJSBS
ZXBvcnRMYWIgZ2VuZXJhdGVkIFBERiBkb2N1bWVudCAtLSBkaWdlc3QgKGh0dHA6Ly93d3cu
cmVwb3J0bGFiLmNvbSkKCi9JbmZvIDcgMCBSCi9Sb290IDYgMCBSCi9TaXplIDExCj4+CnN0
YXJ0eHJlZgoyOTIwCiUlRU9GCg==
--B_3856698950_3788612792--

View File

@ -0,0 +1,127 @@
Content-Type: multipart/mixed; boundary="===============1579291375177179857=="
MIME-Version: 1.0
From: "Sarah Chen (Boardware)" <sarah.chen@boardware.com>
To: "Michael Wang (Boardware)" <michael.wang@boardware.com>
Subject: Q1 2026 Campaign Mockups - Design Review
Date: Thu, 19 Mar 2026 03:22:18 +0800
Message-ID: <campaign2026design@boardware.com>
MIME-Version: 1.0
X-MS-Has-Attach: yes
--===============1579291375177179857==
Content-Type: multipart/alternative; boundary="===============4113167398364688896=="
MIME-Version: 1.0
--===============4113167398364688896==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
SGkgTWljaGFlbCwKClBsZWFzZSBmaW5kIGF0dGFjaGVkIHRoZSBtb2NrdXBzIGZvciB0aGUgUTEg
MjAyNiBjYW1wYWlnbi4gSSd2ZSBpbmNsdWRlZCB0aGUgcHJpbWFyeSBkZXNpZ24gZGlyZWN0aW9u
IHdpdGggY29sb3IgdmFyaWF0aW9ucy4KCktleSBwb2ludHM6Ci0gRGVzaWduIGZvbGxvd3MgdGhl
IHVwZGF0ZWQgYnJhbmQgZ3VpZGVsaW5lcwotIFJlc3BvbnNpdmUgbGF5b3V0IHRlc3RlZCBhY3Jv
c3MgZGV2aWNlcwotIEFsbCBhc3NldHMgYXJlIGluIGhpZ2ggcmVzb2x1dGlvbiAoMzAwIERQSSkK
ClBsZWFzZSByZXZpZXcgYW5kIHByb3ZpZGUgZmVlZGJhY2sgYnkgZW5kIG9mIHdlZWsuCgpCZXN0
IHJlZ2FyZHMsClNhcmFoIENoZW4KRGVzaWduIExlYWQ=
--===============4113167398364688896==
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
PGh0bWw+CjxoZWFkPgo8c3R5bGU+CmJvZHkgeyBmb250LWZhbWlseTogQXB0b3MsIHNhbnMtc2Vy
aWY7IH0KPC9zdHlsZT4KPC9oZWFkPgo8Ym9keT4KPHA+SGkgTWljaGFlbCw8L3A+CjxwPlBsZWFz
ZSBmaW5kIGF0dGFjaGVkIHRoZSBtb2NrdXBzIGZvciB0aGUgUTEgMjAyNiBjYW1wYWlnbi4gSSd2
ZSBpbmNsdWRlZCB0aGUgcHJpbWFyeSBkZXNpZ24gZGlyZWN0aW9uIHdpdGggY29sb3IgdmFyaWF0
aW9ucy48L3A+CjxwPjxzdHJvbmc+S2V5IHBvaW50czo8L3N0cm9uZz48L3A+Cjx1bD4KPGxpPkRl
c2lnbiBmb2xsb3dzIHRoZSB1cGRhdGVkIGJyYW5kIGd1aWRlbGluZXM8L2xpPgo8bGk+UmVzcG9u
c2l2ZSBsYXlvdXQgdGVzdGVkIGFjcm9zcyBkZXZpY2VzPC9saT4KPGxpPkFsbCBhc3NldHMgYXJl
IGluIGhpZ2ggcmVzb2x1dGlvbiAoMzAwIERQSSk8L2xpPgo8L3VsPgo8cD5QbGVhc2UgcmV2aWV3
IGFuZCBwcm92aWRlIGZlZWRiYWNrIGJ5IGVuZCBvZiB3ZWVrLjwvcD4KPHA+QmVzdCByZWdhcmRz
LDxici8+ClNhcmFoIENoZW48YnIvPgpEZXNpZ24gTGVhZDwvcD4KPC9ib2R5Pgo8L2h0bWw+
--===============4113167398364688896==--
--===============1579291375177179857==
Content-Type: image/jpeg; name="Campaign_Mockup_Q1_2026.jpg"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Campaign_Mockup_Q1_2026.jpg"
/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAPERAQEBMQDxATEBUQEBISEA8SEhUPFhIYFhYV
FRMYHSghGBonHhUVITEhMSorLi4uFyAzODMwNyguLisBCgoKDg0OGhAQGy8lIB8uLS8tNy0rLS0t
LS0tLSstLS0tLS0tLSstLS0tLS0tLS0tLS0tLSsrLS0tLSstLS0tLf/AABEIAOEA4QMBIgACEQED
EQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAABgcCBAUDAQj/xABKEAABAwIDAwYICggEBwAAAAABAAID
BBEFEiEGMUEHEyJRYYEUFTJScZGToRczQlNUYpKxstEWI2NygqLS0yTBwvFDZHODlaSz/8QAGAEB
AQEBAQAAAAAAAAAAAAAAAAECAwT/xAAiEQEBAQEAAgIBBQEAAAAAAAAAARECEiExQVETIjJCYQP/
2gAMAwEAAhEDEQA/ALUREXF7xERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBE
RAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARF9QfEREBERARE
QEREBERAREQEREBERAREQEREBERBzdosbioIH1EtyB0WMHlPkPksb+fAAngqubjmN15fPDI6KME5
GRmONmnyW5heQ9ZOl+rcvXbOtdiuJNpI3HmIHGMkbswP66T0i2Qdo7VLIIWxtaxgDWNaGtaNwaNw
WL7rHXWNPYfbl88vgdcAyovljflyZ3jex7dzX9RFgd1gbXnyqjbTAeeb4RCCJ4wC4NuHPY3UEW1z
t3g79OwLawflUYyGNtTFNLM1uV0kZiyvtudYkWJFr8LpOs9Vrm6s1FX3wsUv0ep9cH9a1cT5VY3Q
yCnhmZMWkRvkMWRpPyjZxJtqbddlfKflWe3m2E7pvF9ASJL5JZGeXznGNjvk2HlO4brixUeecVwv
LUCd0rNOcBklljueEjH8PrC3pC6Gw2CmJhqZbmWUdDNcuEZ1zEnXM7eey3WVKJY2vaWuAc1wLXNO
oLSLEFZy3253vK7mzGPRYhTtnj6JvlljvcxygatPrBB4ghdVUxhtY/A67XM+klHSA1Lor6EftGE9
4P1haQV3KzCLiCmkeeBlkZEPU0OKs6n26RY6KqH8omKS/E0jGjr5mol/muAua/bzF3bnMZ+7BEPx
XTzibF0oqUbttjA150HsMNOfuC6LOUHFoQHT08b2EAhxgljBB3dNpt7k84bFtIq+wvlVpn2FTDJT
ni5h55nfYB3uKm2GYrT1Tc9PLHM3jkcCQepw3tPYVqWX4VtoiKgiIgIiICIiAiIgIig+1uB4vNUm
WjqgyHI0Nj56SLKQOloGkOudb9tuClE4XI2txbwKjqJx5bWZY/8AqvORnvIPcoK7DtpotWyultwE
tK//AOgCj21mMYpK2Okr2hjszZWNyMa956TGklhLSLk94Wb1g63J5h+WJ9Q7V0jsjSd+Rp1Pe6/2
QpavDD6UQRRxDdGxrPSQNT3m571nUTsjaXyOaxjRdznGwAVkyOFu16KK0mz7hVl744zFne+5DC1w
dewy9eo9S7+H4nBUAmGRsmXyrXBHVdpse9baqIqzZ53heYxR8zzhffKzLk35cvutZZVmzhNSHsji
ERcx2gYA0NtcZe73rv8AjCHneY5xnPWzc3fpWtf7tbdS2UR9XxamIYnBTgGaRsebyb3JNt9mi5K9
6aoZK0Pjc17Hatc03BU1ceddQQzgNmjbIAbgOG49YPBKahhi0jjjj/dY0H1gLDFp5I4ZHws52Rrb
sZqbm4B0GpsLm3Gyh4jxmr1JdAw9ogHqHTUtxZNTxx6/fuUGxbBZIXHK1z4ibtc0E2HU4DcQvn6D
TSazVDSePRkkPrcQplh9KIIo4g5zhGwMDnbzbiVZb+E6k/KFYdhEs7gMrmMv0nuBAA42vvKlOPUh
fTFkY1Zlc1o3kN4Du+5dNFdTFXyQMfvAPbx9a+4bg9Q55fRuc2RgzZg8xuHU0PHE9R0NlYNRhFPI
7M6NpcdSQXNue2xF1tQQMjaGsaGNHACwv1+lSyVZbHH2a5RXxv8ABsTBY4HLz+XKWn9swaW+sNOy
2qspjg4Agggi4IIIIO4g8Qq+x3BIqxln9F4+LkA6Teztb2fcuJsptHNhE3gVYb0pPRdqRHc6SMPz
Z4jhqesGbZ8u3PWrdRGkEAjUHUEbiEW2hERAREQEREBV3jWK7QGeaOnpw2JsjmxObFG7NHfou5x7
rEkWO4WvZWIilgq621DvOb/48KPYd4TV4mzwx3OzQkiUnm9BCTZvQGXR596uytqRDHJK7yY2OkPo
a0uP3KoOTqJz3VNQ/V7iGk/XcTI/3lqxefcZ69RNlw9scNlqafLFq9sgkyXAzgAiwvx1v3LuItWa
4y4hWxGCVEMz5pWuibzZYGutmcSQd3ACykG0mITU0POQx86c4DrhxDWWN3EN14Adl11UUnOTIt62
7VW+NZfCvD+ZNs97Wfzd+byW5y2+2qnuzWIzVMJkmjERzkNsHAOZYdIB2u8kdtl1rok5xb1v0hW3
GCVE0rJomulbzYjLW2zNIcTe3EG/uXX2NwyWmpy2XRz5DJkuDlBAFjbS+l+9d1E8Zup5eseVU17m
PEbsjyxwY4i4a8jQ29KhfiDFjvqf/am/JTlFbNJ1iDeLcZi1bK6TsE4f7pApXgjqgwsNUA2bXMBl
3X6NwNAbLeRSc4XrRERaZEREBcnaXBm1kJboJW3dC7qdxaT5p3eo8F1kUs1Zcczkp2hL2OoJriSA
Xhvv5kGzoz2sNh6COpWEqY2kLsPr4K6MaOdneBxc3SVv8TD6yVcsMrXta9pzNc0OaetpFwVOb9O8
uslX+3u3UlNJ4JR5TMPjpCA7I47mMbuLraknQaab7SDbXaVmHU5fo6d9207Ot/FxHmtvc9w4qqsL
wCokiNWf1jpHOeAfjHAm5k7S4kn/AHVt+odXI6mE8otdTyN8LtUQk9PoMbIG8SxzAASOojXsVvxS
Ne1rmkOa4BzSNxaRcEL88Yu8BoB3gkkcdBbcr62fpXQ0tLE/y46eNjv3msAPvUnzYnN2a30RFtoR
EQR3lDqObw2rPnRiL2j2s+5xUR2Dhy0bXefI9/qOQfhXb5XZstAG/OVMbfUHP/0BaOy8eWjph+yD
vtdL/NY/sx38OoiItOIiIgIiICIiAiIgIiICIiAiIgIiIOHtpR87SSH5UVpm/wAPlfyly8NnuUKG
kw6KJ7XzVMYdGxgBa3mwTzZdIdAMpA0uejuUhljD2uadQ5paR2EWP3qsqCmjhkY5zQ/K8Eh9nCwO
um5Zy7sb57kntjXz1FdMamruS9oLRlLW83c5WxtO5m/0679SpbhO0bBG4VBDDGy4dbymgbrD5XZx
W3tZDGYC97msLNWOcbX62Drv1ddlCsFwmfFJhDCMsbSDJIRdsbfOd1u32bx9FyHrmf6e+r/ju7F4
Y7Fa99ZK21PC8PI0sZBbmo+0jRzj6POVwrSwbC4qOFlPCMrGDvc46uc48XE6lbismOwiItAiIgr7
lod/haUf83f1QSfmvbBm2p6cdUEf4AvLlnZelpj1Vdj3wSfkvXBX5qanPXBH+ALH9nP/AKfDcREW
nIREQEREBERAREQEREBERAREQEREH0KqTNLPI5lPE+UlzrBjHPdbMdbNGitKZ+Vrnea1zvULrici
kRtWydZhZ3gPcfxBZu7kdOOZWrhXJ9W1bmy4jK6JoGjMzXzZeoW6EY9foVmYThcNJE2GBgjjbwFy
SeLnOOrnHrK20VkkdRERaBERAREQQ/lWp8+HPcNTHNFJ3Zsh9zyuRshNnoqf6rTGf4HFv3AKcY/Q
eFUtRBxkhexvY8t6J9dlV/JxWXjmgNw5jxIAd4DhlcO4t/mWL/Jjv4TBERacRERAREQEREBERARE
QEREBERAREQcraqp5qjqHbiYzGPS85P81v8AJHSZKAycZp5H/wALbRD8BUT5Q6skQUrOk97ucLRv
Jvkjb3kn1K1MEw8UtPBTj/hRNYT1uA6R7zc96zP5O3E9N1ERbbEREBERAREQfVT21lI7CcTFSwHw
ecuksN3SP66P0gkPHpHUVcC5G1eAsxCmfA6zX+XC8i+SYeSfRqQewlZ6mwrixSNe1rmkOa4BzSNx
aRcELJQrZTF3UsjqCrvG5jyxmb5D7/Fk+ab3ad2vUQpqkuuFmUREVZEREBERAREQEREBERAREQFj
I8NBc4hrWgucTuDQLkrJQ7bXFnSEUNODJI9zWyBupLiejEO0mxP+6luRqTaz2IpTiWJvrHg8zARI
0Hdm1EDPSLF57W9qt1cXZDAW4fSsh0Mh/WTuHypiBe3YLBo7AF2k5mR3ERFoEREBERAREQEREEW2
22MjxFudpEVU1tmyEdF7eDJAN46jvF+IuDAKPHarDZPBa+N5DfJO94Zuux26Vnfce5XQorylYPJV
0RELc8kUjZmtAu5zQHNc1vbZ17cctljqfcSzWm2sjMYmztERYH53HK3IdxN929avj+j+kQe0aoHN
j7p6WOgbETICxnRJc52Q3aBHa+bQX9CnOFcl1MYYjUuqGzlgMrWSR5GvOpaLsO7dv4Kbb8MTj8s/
H1H9Ig9qxZePKT6RT+1Z+a9TyV0HzlWP+5D/AG1xtq+TmGmpZJ6V1TLJHlcWOMbgY79MgNYCSBr3
FX9x4RIHyta0vLmtYBmLiQGhvXfdZafjuk+kQe1j/NQOp2jbJQR0bWOzgMYXXBaWtdduUDUk2aLW
UywbkvgfBC+pfUxzuYHSMY6ENa465bFhNwLA677qeVvwThteO6T6RB7WP808d0n0iD2sf5rL4KqH
56r+3B/bT4KqH56r+3B/bT9x+mx8d0n0iD2sf5r1psSgldljlikda+VsjHGw42BWHwVUXz1X9uD+
2ovtNsxJhFRDU0zZZ6dtjmf0i2TUOa8sAytLTobcT3t6h+mlVXilPCcsssUbrXyueAbddl4/pBR/
SIPaNUZ2ewOTGax888ckVLlu5zejqGhrGMe4dI8SbcDuuFMPguw/zqr2sf8AQktp4RqfpBR/SIPa
NXtS4rTzOyRTRSOtfK17SbDfovX4LsP86q9rH/QoztHsZUYdPFU4eyWoiaL2I52RkliCHNaAXNIP
Aaa9iu9Q8Iy2l2kkbIaSla505IaXBtyHOAIbG3i6xGvD7pHyf7E+B/4qqs6rcDlbfMIWu39L5Uhv
q7huHEnk7A4DWSVzsRq43QANcWhzSxzpHNyC0Z1DQ2+/s36qzU5m+63zMERFtRERAREQEREBERAR
EQEREGIiaCXBrQ473WGY96yREBfV8RBpxYTTMkMzYIGynUyNhjD79eYC63ERAREQF9XxEH1fERAR
EQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREB
ERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQ
EREBERAREQEREBERB//Z
--===============1579291375177179857==--

View File

@ -0,0 +1,30 @@
Subject: Test Zip Attachment
From: tester@example.com
To: reviewer@example.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="===============2899700414055881751=="
--===============2899700414055881751==
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Hi,
Please check the attached zip file.
Thanks.
--===============2899700414055881751==
Content-Type: application/zip
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test_data.zip"
MIME-Version: 1.0
UEsDBBQAAAAAACyJc1zvlIadNgAAADYAAAAKAAAAc2VjcmV0LnR4dENPTkZJREVOVElBTDogVEhJ
UyBJUyBBIFNFQ1JFVCBEVU1NWSBaSVAgVEVTVCBGT1IgRExQLlBLAwQUAAAAAAAsiXNcV7unaSkA
AAApAAAABwAAAGNvZGUucHlkZWYgbXlfc2VjcmV0KCk6CiAgICByZXR1cm4gJ3Bhc3N3b3JkMTIz
J1BLAQIUAxQAAAAAACyJc1zvlIadNgAAADYAAAAKAAAAAAAAAAAAAACAAQAAAABzZWNyZXQudHh0
UEsBAhQDFAAAAAAALIlzXFe7p2kpAAAAKQAAAAcAAAAAAAAAAAAAAIABXgAAAGNvZGUucHlQSwUG
AAAAAAIAAgBtAAAArAAAAAAA
--===============2899700414055881751==--

View File

@ -0,0 +1,51 @@
[ Directory of .eml Files ]
┌───────────────┐
│ CLI (cli.py) ├────────────────────────────────────────────────┐
└───────┬───────┘ │
│ Loop through EMLs │
▼ │
┌───────────────┐ Metadata (Subject, From, To, Date) │
│ EML Parser │──────────────────────────────────────────┐ │
│ (parser.py) │ │ │
├───────────────┤ Body Text (Parsed via bs4) │ │
│ - Decode Heads├────────────────────────────────────┐ │ │
│ - Extract HTML│ │ │ │
│ - Save Attach │ Attachment Files ▼ ▼ ▼
│ to Temp Dir ├─────────────┐ ┌────────────────────────┐
└───────────────┘ │ │ Prompt Builder │
▼ │ (analyzer.py) │
┌───────────────┐ ┌───────────────┐ ├────────────────────────┤
│ Converter │◄────┤ Temporary Dir │ │ System Prompt: │
│(converter.py) │ └───────────────┘ │ - DLP Policy rules │
├───────────────┤ │ (from policy.py) │
│ - MarkItDown │ │ - Schemas & Thresholds │
│ - Zip/7z Loop │ │ │
│ - PyMuPDF Ext │ │ User Prompt: │
│ - Text Extrac │ │ - Email Metadata │
│ - Image Base64│ │ - Body Text │
└───────┬───────┘ │ - Multimodal list │
│ │ (Texts + Image URLs) │
│ Converted Texts & IMAGE_SENTINELs └───────────┬────────────┘
└─────────────────────────────────────────────────► │ Use OpenAI-comp
│ API (vLLM/Qwen)
┌────────────────────────┐ (or simulator)
│ LLM Inference │
└───────────┬────────────┘
┌───────────▼────────────┐
│ JSON validation │
│ (analyzer.py) │
├────────────────────────┤
│ Parse: action, risk, │
│ violations, summary │
└───────────┬────────────┘
│ DLPResult
┌────────────────────────┐
│ CLI Output │
├────────────────────────┤
│ - Write JSON to disk │
│ - Rich summary table │
└────────────────────────┘

1
email_dlp/__init__.py Normal file
View File

@ -0,0 +1 @@
# Email DLP - Data Loss Prevention for email content

241
email_dlp/analyzer.py Normal file
View File

@ -0,0 +1,241 @@
"""DLP analysis backends: remote LLM or deterministic local simulation."""
import json
import os
import re
from typing import Any
import openai
from dotenv import load_dotenv
load_dotenv()
from .converter import IMAGE_SENTINEL
from .models import ActionClass, AttachmentResult, DLPResult, RiskLevel, ViolationType
from .policy import format_policy_for_prompt
from .simulator import simulate_analysis
OUTPUT_SCHEMA = {
"type": "object",
"properties": {
"risk_level": {"type": "string", "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW"]},
"risk_score": {"type": "integer", "minimum": 0, "maximum": 100},
"violation_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"PII",
"FINANCIAL_DATA",
"SOURCE_CODE",
"REGULATORY_DOCUMENT",
"LEGAL_CONTRACT",
"PAYROLL_RECORD",
"CUSTOMER_LIST",
"INTERNAL_MEMO",
"NONE",
],
},
},
"action": {"type": "string", "enum": ["PASS", "ALERT", "BLOCK"]},
"summary": {"type": "string"},
"evidence": {"type": "array", "items": {"type": "string"}},
},
"required": ["risk_level", "risk_score", "violation_types", "action", "summary", "evidence"],
}
SYSTEM_PROMPT_TEMPLATE = """\
You are a Data Loss Prevention (DLP) analyst. Your task is to evaluate email content and attachments against the DLP policy below, then return a structured JSON decision.
## DLP Policy
{policy_json}
## Output Schema
Respond with valid JSON only (no markdown fences, no extra text) matching this schema:
{schema_json}
## Critical Rules
1. temperature=0: be deterministic and consistent.
2. evidence: include direct quotes (verbatim excerpts) from the actual email or attachment content that justify your decision. Do not paraphrase.
3. risk_score: assign 0-100. Use the full range — a customer CSV with 500 rows should score 95+, a casual internal memo scores 60-70.
4. action: MUST match the threshold — BLOCK if risk_score>=80, ALERT if risk_score>=40, PASS otherwise.
5. If no policy violations are found, set violation_types=["NONE"], risk_level="LOW", risk_score<40, action="PASS".
"""
def build_system_prompt() -> str:
"""Build the system prompt used for DLP analysis."""
return SYSTEM_PROMPT_TEMPLATE.format(
policy_json=format_policy_for_prompt(),
schema_json=json.dumps(OUTPUT_SCHEMA, indent=2),
)
def _build_user_content(
subject: str,
sender: str,
recipient: str,
date: str,
body_text: str,
attachment_texts: list[tuple[str, str]], # [(filename, text_or_sentinel)]
) -> list[dict[str, Any]]:
"""Build the user message content for the VLM.
Returns a multimodal content list (OpenAI vision format).
Text attachments are embedded as text blocks; image attachments are
inserted as image_url blocks so the VLM can see them directly.
"""
header_block = "\n".join([
"## Email Headers",
f"Subject: {subject}",
f"From: {sender}",
f"To: {recipient}",
f"Date: {date}",
"",
"## Email Body",
body_text or "(empty body)",
])
content: list[dict[str, Any]] = [{"type": "text", "text": header_block}]
for filename, text in attachment_texts:
if text.startswith(IMAGE_SENTINEL):
# IMAGE_SENTINEL format: "__IMAGE__:<mime>:<base64>"
payload = text[len(IMAGE_SENTINEL):] # "<mime>:<base64>"
mime, b64 = payload.split(":", 1)
content.append({"type": "text", "text": f"\n## Attachment: {filename} (image)"})
content.append({
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{b64}"},
})
else:
content.append({
"type": "text",
"text": f"\n## Attachment: {filename}\n{text or '(no extractable text)'}",
})
return content
def _parse_llm_response(content: str) -> dict[str, Any]:
"""Parse JSON from LLM response, handling markdown fences."""
content = content.strip()
# Strip triple-backtick fences
fence_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
if fence_match:
content = fence_match.group(1)
return json.loads(content)
def _map_action(action_str: str) -> ActionClass:
mapping = {"PASS": ActionClass.PASS_, "ALERT": ActionClass.ALERT, "BLOCK": ActionClass.BLOCK}
return mapping.get(action_str.upper(), ActionClass.ALERT)
def analyze_email(
email_file: str,
subject: str,
sender: str,
recipient: str,
date: str,
body_text: str,
attachment_texts: list[tuple[str, str]],
attachment_results: list[AttachmentResult],
processing_errors: list[str],
endpoint: str = "http://localhost:8000/v1",
model: str = "Qwen/Qwen3.5-35B-A3B",
backend: str = "llm",
) -> DLPResult:
"""Analyze email content using an LLM or the deterministic simulator."""
if backend == "simulated":
return simulate_analysis(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
body_text=body_text,
attachment_texts=attachment_texts,
attachment_results=attachment_results,
processing_errors=processing_errors,
)
# Use environment variables as fallback if they exist
final_endpoint = os.getenv("OPENAI_BASE_URL", endpoint)
final_api_key = os.getenv("OPENAI_API_KEY", "not-needed")
final_model = os.getenv("MODEL_NAME", model)
client = openai.OpenAI(base_url=final_endpoint, api_key=final_api_key)
system_prompt = build_system_prompt()
user_content = _build_user_content(
subject=subject,
sender=sender,
recipient=recipient,
date=date,
body_text=body_text,
attachment_texts=attachment_texts,
)
response = client.chat.completions.create(
model=final_model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}, # list[dict] for multimodal
],
temperature=0.0,
max_tokens=1024,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
raw_content = response.choices[0].message.content or ""
try:
parsed = _parse_llm_response(raw_content)
except json.JSONDecodeError as e:
processing_errors.append(f"JSON parse error: {e}; raw={raw_content[:200]}")
# Return a safe fallback
return DLPResult(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
risk_level=RiskLevel.HIGH,
risk_score=60,
violation_types=[ViolationType.NONE],
action=ActionClass.ALERT,
summary="Analysis failed due to JSON parse error.",
evidence=[],
attachments=attachment_results,
processing_errors=processing_errors,
)
# Map string values to enums
risk_level = RiskLevel(parsed.get("risk_level", "HIGH"))
violation_types = [
ViolationType(v) for v in parsed.get("violation_types", ["NONE"])
if v in ViolationType.__members__
]
if not violation_types:
violation_types = [ViolationType.NONE]
action = _map_action(parsed.get("action", "ALERT"))
return DLPResult(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
risk_level=risk_level,
risk_score=int(parsed.get("risk_score", 60)),
violation_types=violation_types,
action=action,
summary=parsed.get("summary", ""),
evidence=parsed.get("evidence", []),
attachments=attachment_results,
processing_errors=processing_errors,
)

550
email_dlp/cli.py Normal file
View File

@ -0,0 +1,550 @@
"""CLI entry point: preview and batch process .eml files through the DLP pipeline."""
import json
from datetime import datetime
from pathlib import Path
from typing import Optional
import typer
from rich.console import Console
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
from rich.table import Table
from .analyzer import _build_user_content, analyze_email, build_system_prompt
from .converter import IMAGE_SENTINEL, convert_attachment
from .models import ActionClass, AttachmentResult, DLPResult
from .parser import parse_eml
from .policy_reviewer import review_corpus
from .simulator import simulate_analysis
app = typer.Typer(help="Email DLP — scan .eml files for data loss prevention policy violations.")
console = Console()
ACTION_COLORS = {
ActionClass.BLOCK: "bold red",
ActionClass.ALERT: "bold yellow",
ActionClass.PASS_: "bold green",
}
RISK_COLORS = {
"CRITICAL": "bold red",
"HIGH": "red",
"MEDIUM": "yellow",
"LOW": "green",
}
def _process_single_email(
eml_path: Path,
endpoint: str,
model: str,
backend: str = "llm",
) -> DLPResult:
"""Parse, convert, and analyze one .eml file."""
processing_errors: list[str] = []
# 1. Parse MIME
parsed = parse_eml(eml_path)
try:
# 2. Convert attachments
attachment_texts: list[tuple[str, str]] = []
attachment_results: list[AttachmentResult] = []
for att in parsed.attachments:
entries = convert_attachment(att.path, att.filename)
for display_name, text, status in entries:
if "truncated" in status:
processing_errors.append(
f"'{display_name}' truncated to 20000 chars"
)
attachment_texts.append((display_name, text))
attachment_results.append(
AttachmentResult(
filename=display_name,
content_type=att.content_type,
extracted_text_chars=0 if text.startswith(IMAGE_SENTINEL) else len(text),
conversion_status=status.split("|")[0],
)
)
# 3. Analyze with LLM
result = analyze_email(
email_file=eml_path.name,
subject=parsed.subject,
sender=parsed.sender,
recipient=parsed.recipient,
date=parsed.date,
body_text=parsed.body_text,
attachment_texts=attachment_texts,
attachment_results=attachment_results,
processing_errors=processing_errors,
endpoint=endpoint,
model=model,
backend=backend,
)
finally:
parsed.cleanup()
return result
def _simulate_single_email(eml_path: Path) -> DLPResult:
"""Parse, convert, and simulate one .eml file without an LLM."""
processing_errors: list[str] = []
parsed = parse_eml(eml_path)
try:
attachment_texts: list[tuple[str, str]] = []
attachment_results: list[AttachmentResult] = []
for att in parsed.attachments:
entries = convert_attachment(att.path, att.filename)
for display_name, text, status in entries:
if "truncated" in status:
processing_errors.append(
f"'{display_name}' truncated to 20000 chars"
)
attachment_texts.append((display_name, text))
attachment_results.append(
AttachmentResult(
filename=display_name,
content_type=att.content_type,
extracted_text_chars=0 if text.startswith(IMAGE_SENTINEL) else len(text),
conversion_status=status.split("|")[0],
)
)
result = simulate_analysis(
email_file=eml_path.name,
subject=parsed.subject,
sender=parsed.sender,
recipient=parsed.recipient,
date=parsed.date,
body_text=parsed.body_text,
attachment_texts=attachment_texts,
attachment_results=attachment_results,
processing_errors=processing_errors,
)
finally:
parsed.cleanup()
return result
def _preview_single_email(
eml_path: Path,
include_system_prompt: bool = True,
include_full_prompt: bool = False,
) -> dict:
"""Parse and convert one .eml file without calling the LLM."""
parsed = parse_eml(eml_path)
try:
attachments_preview: list[dict[str, object]] = []
attachment_texts: list[tuple[str, str]] = []
processing_errors: list[str] = []
for att in parsed.attachments:
entries = convert_attachment(att.path, att.filename)
for display_name, text, status in entries:
is_image = text.startswith(IMAGE_SENTINEL)
if "truncated" in status:
processing_errors.append(
f"'{display_name}' truncated to 20000 chars"
)
attachment_texts.append((display_name, text))
attachments_preview.append(
{
"filename": display_name,
"content_type": att.content_type,
"conversion_status": status,
"is_image": is_image,
"extracted_text_chars": 0 if is_image else len(text),
"text_preview": (
None
if is_image
else text[:500]
),
"image_data_url_preview": (
None
if not is_image
else text[:120] + "..."
if len(text) > 120
else text
),
}
)
llm_user_content = _build_user_content(
subject=parsed.subject,
sender=parsed.sender,
recipient=parsed.recipient,
date=parsed.date,
body_text=parsed.body_text,
attachment_texts=attachment_texts,
)
llm_user_content_preview: list[dict[str, object]] = []
for block in llm_user_content:
if block["type"] == "text":
llm_user_content_preview.append(
{
"type": "text",
"text_preview": str(block["text"])[:1000],
"text_chars": len(str(block["text"])),
}
)
else:
url = str(block["image_url"]["url"])
llm_user_content_preview.append(
{
"type": "image_url",
"url_preview": url[:120] + ("..." if len(url) > 120 else ""),
"url_chars": len(url),
}
)
preview_result = {
"email_file": eml_path.name,
"subject": parsed.subject,
"sender": parsed.sender,
"recipient": parsed.recipient,
"date": parsed.date,
"body_text_chars": len(parsed.body_text),
"body_text_preview": parsed.body_text[:1000],
"attachment_count": len(parsed.attachments),
"attachments": attachments_preview,
"processing_errors": processing_errors,
"llm_user_content_preview": llm_user_content_preview,
}
if include_system_prompt:
system_prompt = build_system_prompt()
preview_result["llm_system_prompt_preview"] = {
"text_preview": system_prompt[:2000],
"text_chars": len(system_prompt),
}
if include_full_prompt:
preview_result["llm_system_prompt"] = system_prompt
if include_full_prompt:
preview_result["llm_user_content"] = llm_user_content
return preview_result
finally:
parsed.cleanup()
@app.command()
def analyze(
input_dir: Path = typer.Option(
Path("data"),
"--input",
"-i",
help="Directory containing .eml files",
),
output_dir: Optional[Path] = typer.Option(
None,
"--output",
"-o",
help="Directory to write JSON results (defaults to output/analyze-TIMESTAMP)",
),
endpoint: str = typer.Option(
"http://localhost:8000/v1",
"--endpoint",
help="vLLM OpenAI-compatible endpoint",
),
model: str = typer.Option(
"Qwen/Qwen3.5-35B-A3B",
"--model",
help="Model name to use for analysis",
),
backend: str = typer.Option(
"llm",
"--backend",
help="Analysis backend: 'llm' for API calls, 'simulated' for local deterministic analysis",
),
summary: bool = typer.Option(
False,
"--summary",
"-s",
help="Print a summary table after processing",
),
) -> None:
"""Batch analyze all .eml files in INPUT_DIR and write JSON results to OUTPUT_DIR."""
if output_dir is None:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
output_dir = Path("output") / f"analyze-{timestamp}"
eml_files = sorted(input_dir.glob("*.eml"))
if not eml_files:
console.print(f"[red]No .eml files found in {input_dir}[/red]")
raise typer.Exit(1)
output_dir.mkdir(parents=True, exist_ok=True)
results: list[DLPResult] = []
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("{task.completed}/{task.total}"),
TimeElapsedColumn(),
console=console,
) as progress:
task = progress.add_task("Analyzing emails...", total=len(eml_files))
for eml_path in eml_files:
progress.update(task, description=f"[cyan]{eml_path.name[:50]}[/cyan]")
try:
result = _process_single_email(eml_path, endpoint, model, backend=backend)
except Exception as e:
console.print(f"[red]Error processing {eml_path.name}: {e}[/red]")
progress.advance(task)
continue
# Write individual JSON result
out_file = output_dir / (eml_path.stem + ".json")
out_file.write_text(result.model_dump_json(indent=2))
results.append(result)
progress.advance(task)
# Write batch summary
batch_summary = {
"total": len(results),
"by_action": {
"BLOCK": sum(1 for r in results if r.action == ActionClass.BLOCK),
"ALERT": sum(1 for r in results if r.action == ActionClass.ALERT),
"PASS": sum(1 for r in results if r.action == ActionClass.PASS_),
},
"by_risk": {
level: sum(1 for r in results if r.risk_level.value == level)
for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
},
"emails": [
{
"file": r.email_file,
"subject": r.subject,
"risk_level": r.risk_level.value,
"risk_score": r.risk_score,
"action": r.action.value,
"violation_types": [v.value for v in r.violation_types],
}
for r in results
],
}
(output_dir / "batch_summary.json").write_text(
json.dumps(batch_summary, indent=2)
)
console.print(f"\n[bold green]Done![/bold green] Processed {len(results)}/{len(eml_files)} emails.")
console.print(f"Results written to: [cyan]{output_dir}/[/cyan]")
if summary and results:
_print_summary_table(results)
@app.command()
def preview(
input_dir: Path = typer.Option(
Path("data"),
"--input",
"-i",
help="Directory containing .eml files",
),
output_dir: Optional[Path] = typer.Option(
None,
"--output",
"-o",
help="Optional directory to write preview JSON files",
),
print_json: bool = typer.Option(
False,
"--print-json",
help="Print preview JSON to stdout",
),
include_system_prompt: bool = typer.Option(
True,
"--include-system-prompt/--no-system-prompt",
help="Include the analyzer system prompt built from policy.py",
),
include_full_prompt: bool = typer.Option(
False,
"--include-full-prompt",
help="Include the full system prompt and full user content in JSON output",
),
) -> None:
"""Preview parsed email and converted attachment content before LLM analysis."""
eml_files = sorted(input_dir.glob("*.eml"))
if not eml_files:
console.print(f"[red]No .eml files found in {input_dir}[/red]")
raise typer.Exit(1)
if output_dir is not None:
output_dir.mkdir(parents=True, exist_ok=True)
previews: list[dict] = []
for eml_path in eml_files:
try:
preview_result = _preview_single_email(
eml_path,
include_system_prompt=include_system_prompt,
include_full_prompt=include_full_prompt,
)
except Exception as e:
console.print(f"[red]Error previewing {eml_path.name}: {e}[/red]")
continue
previews.append(preview_result)
if output_dir is not None:
out_file = output_dir / f"{eml_path.stem}.preview.json"
out_file.write_text(json.dumps(preview_result, indent=2))
if output_dir is not None:
batch_file = output_dir / "batch_preview.json"
batch_file.write_text(json.dumps(previews, indent=2))
console.print(f"[green]Preview JSON written to[/green] [cyan]{output_dir}[/cyan]")
if print_json:
console.print_json(json.dumps(previews, indent=2))
else:
table = Table(title="Email Preview Results", show_lines=True)
table.add_column("File", style="dim", max_width=45)
table.add_column("Body Chars", justify="right")
table.add_column("Attachments", justify="right")
table.add_column("Errors", justify="right")
for preview_result in previews:
table.add_row(
str(preview_result["email_file"]),
str(preview_result["body_text_chars"]),
str(preview_result["attachment_count"]),
str(len(preview_result["processing_errors"])),
)
console.print(table)
@app.command()
def simulate(
input_dir: Path = typer.Option(
Path("data"),
"--input",
"-i",
help="Directory containing .eml files",
),
output_dir: Optional[Path] = typer.Option(
None,
"--output",
"-o",
help="Directory to write simulated JSON results (defaults to output/simulated-TIMESTAMP)",
),
summary: bool = typer.Option(
True,
"--summary/--no-summary",
help="Print a summary table after processing",
),
) -> None:
"""Batch simulate DLP analysis locally without calling an LLM."""
if output_dir is None:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
output_dir = Path("output") / f"simulated-{timestamp}"
eml_files = sorted(input_dir.glob("*.eml"))
if not eml_files:
console.print(f"[red]No .eml files found in {input_dir}[/red]")
raise typer.Exit(1)
output_dir.mkdir(parents=True, exist_ok=True)
results: list[DLPResult] = []
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("{task.completed}/{task.total}"),
TimeElapsedColumn(),
console=console,
) as progress:
task = progress.add_task("Simulating email analysis...", total=len(eml_files))
for eml_path in eml_files:
progress.update(task, description=f"[cyan]{eml_path.name[:50]}[/cyan]")
try:
result = _simulate_single_email(eml_path)
except Exception as e:
console.print(f"[red]Error processing {eml_path.name}: {e}[/red]")
progress.advance(task)
continue
out_file = output_dir / (eml_path.stem + ".json")
out_file.write_text(result.model_dump_json(indent=2))
results.append(result)
progress.advance(task)
batch_summary = {
"total": len(results),
"generator": "local-simulator",
"model_label": "gpt-5.4-simulated",
"by_action": {
"BLOCK": sum(1 for r in results if r.action == ActionClass.BLOCK),
"ALERT": sum(1 for r in results if r.action == ActionClass.ALERT),
"PASS": sum(1 for r in results if r.action == ActionClass.PASS_),
},
"by_risk": {
level: sum(1 for r in results if r.risk_level.value == level)
for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
},
"emails": [
{
"file": r.email_file,
"subject": r.subject,
"risk_level": r.risk_level.value,
"risk_score": r.risk_score,
"action": r.action.value,
"violation_types": [v.value for v in r.violation_types],
}
for r in results
],
}
(output_dir / "batch_summary.json").write_text(
json.dumps(batch_summary, indent=2)
)
console.print(
f"\n[bold green]Done![/bold green] Simulated {len(results)}/{len(eml_files)} emails."
)
console.print(f"Results written to: [cyan]{output_dir}/[/cyan]")
if summary and results:
_print_summary_table(results)
def _print_summary_table(results: list[DLPResult]) -> None:
"""Print a rich summary table to the console."""
table = Table(title="Email DLP Analysis Results", show_lines=True)
table.add_column("File", style="dim", max_width=45)
table.add_column("Risk Level", justify="center")
table.add_column("Score", justify="center")
table.add_column("Action", justify="center")
table.add_column("Violations", max_width=40)
for r in results:
risk_color = RISK_COLORS.get(r.risk_level.value, "white")
action_color = ACTION_COLORS.get(r.action, "white")
violations = ", ".join(v.value for v in r.violation_types)
table.add_row(
r.email_file,
f"[{risk_color}]{r.risk_level.value}[/{risk_color}]",
str(r.risk_score),
f"[{action_color}]{r.action.value}[/{action_color}]",
violations,
)
console.print(table)

238
email_dlp/converter.py Normal file
View File

@ -0,0 +1,238 @@
"""Attachment → markdown text conversion routing."""
import base64
import tempfile
import zipfile
from pathlib import Path
from markitdown import MarkItDown
MAX_TEXT_CHARS = 20_000
# Sentinel prefix used to pass image data through the (text, status) interface.
# Format: IMAGE_SENTINEL + "<mime_type>:<base64_data>"
IMAGE_SENTINEL = "__IMAGE__:"
_IMAGE_MIME = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".gif": "image/gif",
".bmp": "image/bmp", ".tiff": "image/tiff", ".webp": "image/webp",
".img": "image/png", # fallback for generated inline names
}
def _convert_single_file(filepath: Path) -> tuple[str, str]:
"""Convert a single file to text. Returns (text, status).
For image files, text is IMAGE_SENTINEL + "<mime>:<base64>" and
status is "ok:image". Callers must check for the sentinel.
"""
suffix = filepath.suffix.lower()
# Image — return base64 sentinel for VLM consumption
if suffix in _IMAGE_MIME:
mime = _IMAGE_MIME[suffix]
b64 = base64.b64encode(filepath.read_bytes()).decode()
return IMAGE_SENTINEL + f"{mime}:{b64}", "ok:image"
known_binary_exts = {
".py", ".js", ".ts", ".java", ".c", ".cpp", ".h", ".cs",
".go", ".rb", ".rs", ".sh", ".txt", ".md", ".sql", ".yaml", ".yml",
".json", ".xml", ".html", ".htm", ".css",
}
if suffix in known_binary_exts:
# Plain text fallback — read directly
try:
text = filepath.read_text(errors="replace")
return text, "ok"
except Exception as e:
return "", f"failed: {e}"
# Use markitdown for PDF, DOCX, XLSX, CSV, etc.
try:
md = MarkItDown()
result = md.convert(str(filepath))
return result.text_content or "", "ok"
except Exception as e:
# Fallback to plain-text read for unknown types
try:
text = filepath.read_text(errors="replace")
return text, f"fallback: {e}"
except Exception as e2:
return "", f"failed: {e2}"
_OFFICE_MEDIA_DIRS = {
".docx": "word/media/",
".pptx": "ppt/media/",
".xlsx": "xl/media/",
}
_IMAGE_EXTS = set(_IMAGE_MIME.keys())
def _extract_pdf_images(
filepath: Path, filename: str
) -> list[tuple[str, str, str]]:
"""Extract embedded images from a PDF using PyMuPDF.
Returns list of (display_name, IMAGE_SENTINEL+..., "ok:image").
Returns empty list if fitz is not installed or no images found.
"""
try:
import fitz # PyMuPDF
except ImportError:
return []
results: list[tuple[str, str, str]] = []
try:
doc = fitz.open(str(filepath))
img_index = 0
for page in doc:
for img in page.get_images():
xref = img[0]
img_data = doc.extract_image(xref)
ext = img_data.get("ext", "png")
mime = _IMAGE_MIME.get(f".{ext}", f"image/{ext}")
b64 = base64.b64encode(img_data["image"]).decode()
display_name = f"{filename}/image_{img_index}.{ext}"
results.append((display_name, IMAGE_SENTINEL + f"{mime}:{b64}", "ok:image"))
img_index += 1
except Exception:
pass
return results
def _extract_office_images(
filepath: Path, filename: str
) -> list[tuple[str, str, str]]:
"""Extract embedded images from a DOCX/PPTX/XLSX using zipfile.
Returns list of (display_name, IMAGE_SENTINEL+..., "ok:image").
Returns empty list if the file is not a valid ZIP or has no images.
"""
suffix = Path(filename).suffix.lower()
media_dir = _OFFICE_MEDIA_DIRS.get(suffix)
if not media_dir:
return []
results: list[tuple[str, str, str]] = []
try:
with zipfile.ZipFile(str(filepath), "r") as zf:
for name in sorted(zf.namelist()):
if not name.startswith(media_dir):
continue
member_suffix = Path(name).suffix.lower()
if member_suffix not in _IMAGE_EXTS:
continue
mime = _IMAGE_MIME[member_suffix]
b64 = base64.b64encode(zf.read(name)).decode()
display_name = f"{filename}/{Path(name).name}"
results.append((display_name, IMAGE_SENTINEL + f"{mime}:{b64}", "ok:image"))
except Exception:
pass
return results
def _convert_7z(
filepath: Path, archive_name: str
) -> list[tuple[str, str, str]]:
"""Extract a .7z archive and convert each member.
Returns list of (display_name, text_or_sentinel, status), one entry per member.
display_name uses "archive.7z/member.ext" format.
"""
try:
import py7zr
except ImportError:
return [(archive_name, "", "failed: py7zr not installed")]
results: list[tuple[str, str, str]] = []
with tempfile.TemporaryDirectory(prefix="email_dlp_7z_") as tmpdir:
tmp = Path(tmpdir)
try:
with py7zr.SevenZipFile(str(filepath), mode="r") as archive:
archive.extractall(path=str(tmp))
except Exception as e:
return [(archive_name, "", f"failed: 7z extraction error: {e}")]
for member_path in sorted(tmp.rglob("*")):
if not member_path.is_file():
continue
display_name = f"{archive_name}/{member_path.name}"
text, status = _convert_single_file(member_path)
if not text.startswith(IMAGE_SENTINEL) and len(text) > MAX_TEXT_CHARS:
text = text[:MAX_TEXT_CHARS]
status = f"{status}|truncated_at_{MAX_TEXT_CHARS}"
results.append((display_name, text, status))
return results if results else [(archive_name, "", "skipped")]
def _convert_zip(
filepath: Path, archive_name: str
) -> list[tuple[str, str, str]]:
"""Extract a .zip archive and convert each member.
Returns list of (display_name, text_or_sentinel, status), one entry per member.
display_name uses "archive.zip/member.ext" format.
"""
import zipfile
results: list[tuple[str, str, str]] = []
with tempfile.TemporaryDirectory(prefix="email_dlp_zip_") as tmpdir:
tmp = Path(tmpdir)
try:
with zipfile.ZipFile(str(filepath), mode="r") as archive:
archive.extractall(path=str(tmp))
except Exception as e:
return [(archive_name, "", f"failed: zip extraction error: {e}")]
for member_path in sorted(tmp.rglob("*")):
if not member_path.is_file():
continue
display_name = f"{archive_name}/{member_path.name}"
text, status = _convert_single_file(member_path)
if not text.startswith(IMAGE_SENTINEL) and len(text) > MAX_TEXT_CHARS:
text = text[:MAX_TEXT_CHARS]
status = f"{status}|truncated_at_{MAX_TEXT_CHARS}"
results.append((display_name, text, status))
return results if results else [(archive_name, "", "skipped")]
def convert_attachment(
filepath: Path, filename: str
) -> list[tuple[str, str, str]]:
"""Convert an attachment file for LLM analysis.
Returns list of (display_name, text_or_sentinel, status).
- Non-archive files: single-element list.
- .7z archives: one element per member file inside the archive.
text_or_sentinel is either plain text or IMAGE_SENTINEL + "<mime>:<base64>"
for image files. Text is truncated to MAX_TEXT_CHARS (images are not truncated).
"""
suffix = Path(filename).suffix.lower()
if suffix == ".7z":
return _convert_7z(filepath, filename)
elif suffix == ".zip":
return _convert_zip(filepath, filename)
text, status = _convert_single_file(filepath)
if not text.startswith(IMAGE_SENTINEL) and len(text) > MAX_TEXT_CHARS:
text = text[:MAX_TEXT_CHARS]
status = f"{status}|truncated_at_{MAX_TEXT_CHARS}"
results = [(filename, text, status)]
# For PDF and Office files, also extract embedded images
if suffix == ".pdf":
results.extend(_extract_pdf_images(filepath, filename))
elif suffix in _OFFICE_MEDIA_DIRS:
results.extend(_extract_office_images(filepath, filename))
return results

52
email_dlp/models.py Normal file
View File

@ -0,0 +1,52 @@
"""Pydantic output schema for DLP analysis results."""
from enum import Enum
from pydantic import BaseModel, Field
class RiskLevel(str, Enum):
CRITICAL = "CRITICAL"
HIGH = "HIGH"
MEDIUM = "MEDIUM"
LOW = "LOW"
class ViolationType(str, Enum):
PII = "PII"
FINANCIAL_DATA = "FINANCIAL_DATA"
SOURCE_CODE = "SOURCE_CODE"
REGULATORY_DOCUMENT = "REGULATORY_DOCUMENT"
LEGAL_CONTRACT = "LEGAL_CONTRACT"
PAYROLL_RECORD = "PAYROLL_RECORD"
CUSTOMER_LIST = "CUSTOMER_LIST"
INTERNAL_MEMO = "INTERNAL_MEMO"
NONE = "NONE"
class ActionClass(str, Enum):
PASS_ = "PASS"
ALERT = "ALERT"
BLOCK = "BLOCK"
class AttachmentResult(BaseModel):
filename: str
content_type: str
extracted_text_chars: int = 0
conversion_status: str = "ok" # "ok" | "failed" | "skipped"
class DLPResult(BaseModel):
email_file: str
subject: str
sender: str
recipient: str
date: str
risk_level: RiskLevel
risk_score: int = Field(ge=0, le=100)
violation_types: list[ViolationType]
action: ActionClass
summary: str
evidence: list[str]
attachments: list[AttachmentResult]
processing_errors: list[str] = Field(default_factory=list)

199
email_dlp/parser.py Normal file
View File

@ -0,0 +1,199 @@
"""MIME email parsing: extract headers, body text, and attachments."""
import email
import email.policy
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from bs4 import BeautifulSoup
@dataclass
class ParsedAttachment:
filename: str
path: Path
content_type: str
@dataclass
class ParsedEmail:
subject: str
sender: str
recipient: str
date: str
body_text: str
attachments: list[ParsedAttachment] = field(default_factory=list)
# tempdir must be kept alive by the caller
_tempdir: tempfile.TemporaryDirectory | None = field(default=None, repr=False)
def cleanup(self) -> None:
if self._tempdir is not None:
self._tempdir.cleanup()
self._tempdir = None
def _decode_header_value(value: str | None) -> str:
if value is None:
return ""
# Decode RFC2047 encoded words (e.g. =?Windows-1252?Q?...?=)
decoded_parts = email.header.decode_header(str(value))
result = ""
for chunk, charset in decoded_parts:
if isinstance(chunk, bytes):
result += chunk.decode(charset or "utf-8", errors="replace")
else:
result += chunk
return result.strip()
def _extract_body(msg: email.message.Message) -> str:
"""Walk MIME parts and extract the best plain-text body."""
plain_parts: list[str] = []
html_parts: list[str] = []
if msg.is_multipart():
for part in msg.walk():
ct = part.get_content_type()
disposition = str(part.get("Content-Disposition", ""))
# Skip attachments
if "attachment" in disposition:
continue
if ct == "text/plain":
payload = part.get_payload(decode=True)
if payload:
charset = part.get_content_charset() or "utf-8"
plain_parts.append(payload.decode(charset, errors="replace"))
elif ct == "text/html":
payload = part.get_payload(decode=True)
if payload:
charset = part.get_content_charset() or "utf-8"
html_parts.append(payload.decode(charset, errors="replace"))
else:
ct = msg.get_content_type()
payload = msg.get_payload(decode=True)
if payload:
charset = msg.get_content_charset() or "utf-8"
text = payload.decode(charset, errors="replace")
if ct == "text/plain":
plain_parts.append(text)
elif ct == "text/html":
html_parts.append(text)
if plain_parts:
return "\n\n".join(plain_parts).strip()
# Fall back to HTML → plain text via BeautifulSoup
if html_parts:
combined_html = "\n".join(html_parts)
soup = BeautifulSoup(combined_html, "html.parser")
return soup.get_text(separator="\n").strip()
return ""
_IMAGE_CONTENT_TYPES = {
"image/jpeg", "image/png", "image/gif",
"image/bmp", "image/tiff", "image/webp",
}
_IMAGE_EXTS = {
"image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif",
"image/bmp": ".bmp", "image/tiff": ".tiff", "image/webp": ".webp",
}
def _collect_attachments(
msg: email.message.Message, tmpdir: Path
) -> list[ParsedAttachment]:
"""Extract all attachment parts and write them to tmpdir.
Also captures inline images (CID-embedded) that have no filename.
"""
attachments: list[ParsedAttachment] = []
seen_names: set[str] = set()
inline_image_counter = 0
for part in msg.walk():
disposition = str(part.get("Content-Disposition", ""))
content_type = part.get_content_type()
filename = part.get_filename()
# Inline image without a filename — generate one from Content-ID or counter
if filename is None and content_type in _IMAGE_CONTENT_TYPES:
cid = str(part.get("Content-ID", "")).strip("<>").split("@")[0]
ext = _IMAGE_EXTS.get(content_type, ".img")
filename = f"inline_{cid or inline_image_counter}{ext}"
inline_image_counter += 1
elif filename is None and "attachment" not in disposition:
continue
elif filename is None:
# Unnamed non-image attachment — skip
continue
# Decode RFC2047 filename if needed
decoded_parts = email.header.decode_header(filename)
filename_clean = ""
for chunk, charset in decoded_parts:
if isinstance(chunk, bytes):
filename_clean += chunk.decode(charset or "utf-8", errors="replace")
else:
filename_clean += chunk
# Avoid duplicates
base_name = filename_clean
counter = 1
while filename_clean in seen_names:
stem = Path(base_name).stem
suffix = Path(base_name).suffix
filename_clean = f"{stem}_{counter}{suffix}"
counter += 1
seen_names.add(filename_clean)
payload = part.get_payload(decode=True)
if payload is None:
continue
dest = tmpdir / filename_clean
dest.write_bytes(payload)
attachments.append(
ParsedAttachment(
filename=filename_clean,
path=dest,
content_type=part.get_content_type(),
)
)
return attachments
def parse_eml(eml_path: Path) -> ParsedEmail:
"""Parse an .eml file and return a ParsedEmail object.
The caller is responsible for calling parsed_email.cleanup() when done,
or using ParsedEmail as a context manager is not implemented — keep
the return value alive until you no longer need the attachment paths.
"""
with open(eml_path, "rb") as f:
msg = email.message_from_binary_file(f, policy=email.policy.compat32)
subject = _decode_header_value(msg.get("Subject"))
sender = _decode_header_value(msg.get("From"))
recipient = _decode_header_value(msg.get("To"))
date = _decode_header_value(msg.get("Date"))
body_text = _extract_body(msg)
tmpdir_obj = tempfile.TemporaryDirectory(prefix="email_dlp_")
tmpdir = Path(tmpdir_obj.name)
attachments = _collect_attachments(msg, tmpdir)
return ParsedEmail(
subject=subject,
sender=sender,
recipient=recipient,
date=date,
body_text=body_text,
attachments=attachments,
_tempdir=tmpdir_obj,
)

132
email_dlp/policy.py Normal file
View File

@ -0,0 +1,132 @@
"""DLP policy definitions: violation categories, thresholds, and prompt formatting."""
import json
from .models import ActionClass, RiskLevel
# Risk score thresholds
RISK_THRESHOLDS = {
RiskLevel.CRITICAL: 80,
RiskLevel.HIGH: 60,
RiskLevel.MEDIUM: 40,
RiskLevel.LOW: 0,
}
# Action mapping based on risk level
RISK_TO_ACTION = {
RiskLevel.CRITICAL: ActionClass.BLOCK,
RiskLevel.HIGH: ActionClass.ALERT,
RiskLevel.MEDIUM: ActionClass.ALERT,
RiskLevel.LOW: ActionClass.PASS_,
}
DLP_CATEGORIES = {
"PII": {
"description": "Personally Identifiable Information",
"signals": [
"Full name combined with email address",
"Social Security Number (SSN) or employee ID",
"Phone numbers combined with personal details",
"Home address combined with personal identifiers",
],
"risk_weight": "HIGH to CRITICAL depending on volume",
},
"FINANCIAL_DATA": {
"description": "Non-public financial information",
"signals": [
"Revenue targets, EBITDA projections, internal forecasts",
"Salary figures, compensation plans",
"Invoice amounts and vendor payment terms",
"Internal budget allocations",
],
"risk_weight": "MEDIUM to CRITICAL depending on sensitivity",
},
"SOURCE_CODE": {
"description": "Proprietary source code or model weights",
"signals": [
"Python, Java, or other source files with copyright notices",
"Internal class names and proprietary algorithms",
"Model architecture files or weight files",
"Internal API keys or credentials embedded in code",
],
"risk_weight": "CRITICAL",
},
"REGULATORY_DOCUMENT": {
"description": "Internal regulatory and compliance drafts",
"signals": [
"CFPB, GDPR, or SOX compliance drafts marked internal",
"Audit findings or remediation plans",
"Internal compliance assessments not yet published",
"Regulatory submission drafts",
],
"risk_weight": "CRITICAL",
},
"LEGAL_CONTRACT": {
"description": "Executed or draft legal agreements",
"signals": [
"Non-Disclosure Agreements (NDAs) with named parties",
"Signed contracts with dates and signatures",
"Settlement agreements or legal memoranda",
"Vendor contracts with financial terms",
],
"risk_weight": "HIGH to CRITICAL",
},
"PAYROLL_RECORD": {
"description": "Employee payroll and compensation records",
"signals": [
"Employee ID combined with salary and payroll period",
"Direct deposit details or bank account information",
"Year-to-date earnings and deductions",
"HR compensation reports",
],
"risk_weight": "CRITICAL",
},
"CUSTOMER_LIST": {
"description": "Customer or prospect data in bulk",
"signals": [
"CSV or table with customer names, emails, and revenue figures",
"CRM exports with contact details",
"Prospect lists for sales campaigns",
"Customer PII in aggregate",
],
"risk_weight": "CRITICAL",
},
"INTERNAL_MEMO": {
"description": "Confidential internal communications",
"signals": [
'Documents marked "INTERNAL ONLY" or "DO NOT DISTRIBUTE"',
"CEO or executive strategy memos",
"Organizational restructuring plans",
"Internal performance reviews or headcount discussions",
],
"risk_weight": "HIGH",
},
}
ACTION_THRESHOLDS = {
"BLOCK": "risk_score >= 80 (CRITICAL risk)",
"ALERT": "risk_score >= 40 (MEDIUM or HIGH risk)",
"PASS": "risk_score < 40 (LOW risk)",
}
def format_policy_for_prompt() -> str:
"""Format the DLP policy as a JSON string for injection into the LLM system prompt."""
policy = {
"categories": DLP_CATEGORIES,
"risk_score_thresholds": {
"CRITICAL": "score >= 80",
"HIGH": "score >= 60",
"MEDIUM": "score >= 40",
"LOW": "score < 40",
},
"action_mapping": ACTION_THRESHOLDS,
"instructions": (
"Evaluate the email against ALL categories above. "
"Assign a risk_score from 0 to 100 based on the most severe violation found. "
"Multiple violations increase the score. "
"action must match the threshold: BLOCK if score>=80, ALERT if score>=40, PASS otherwise. "
"evidence must be direct quotes from the actual email or attachment content."
),
}
return json.dumps(policy, indent=2)

View File

@ -0,0 +1,285 @@
"""Policy-based DLP review derived from DLP_CATEGORIES in policy.py."""
from __future__ import annotations
import re
from collections import defaultdict
from .models import ActionClass, AttachmentResult, DLPResult, RiskLevel, ViolationType
from .policy import DLP_CATEGORIES
# Keywords derived from DLP_CATEGORIES signal descriptions in policy.py
_POLICY_KEYWORDS: dict[ViolationType, dict] = {
ViolationType.PII: {
"keywords": [
"full name",
"email address",
"social security",
"ssn",
"employee id",
"phone number",
"home address",
"personal identifier",
"date of birth",
],
"min_matches": 2,
"base_score": 55,
},
ViolationType.FINANCIAL_DATA: {
"keywords": [
"revenue",
"ebitda",
"projection",
"forecast",
"salary",
"compensation plan",
"invoice",
"amount due",
"payment terms",
"budget",
"gross margin",
"sales data",
],
"min_matches": 1,
"base_score": 50,
},
ViolationType.SOURCE_CODE: {
"keywords": [
"copyright",
"def ",
"class ",
"from __future__",
"import ",
"model weights",
"api key",
"api_key",
"proprietary",
"source code",
"internal source",
],
"min_matches": 2,
"base_score": 85,
},
ViolationType.REGULATORY_DOCUMENT: {
"keywords": [
"cfpb",
"gdpr",
"sox",
"compliance draft",
"not for public release",
"not for public distribution",
"regulatory submission",
"audit findings",
"remediation plan",
"internal compliance",
],
"min_matches": 1,
"base_score": 82,
},
ViolationType.LEGAL_CONTRACT: {
"keywords": [
"non-disclosure",
"nondisclosure",
"nda",
"disclosing party",
"receiving party",
"confidentiality agreement",
"settlement agreement",
"executed contract",
"signed contract",
],
"min_matches": 1,
"base_score": 65,
},
ViolationType.PAYROLL_RECORD: {
"keywords": [
"payroll",
"pay period",
"pay stub",
"direct deposit",
"routing number",
"bank account",
"net pay",
"gross pay",
"tax deductions",
"year-to-date",
"ytd",
"compensation record",
],
"min_matches": 1,
"base_score": 88,
},
ViolationType.CUSTOMER_LIST: {
"keywords": [
"customer list",
"customer_id",
"customer id",
"crm export",
"prospect list",
"top-tier prospect",
"annual_sales",
"company_name",
"bulk export",
"sales campaign",
],
"min_matches": 2,
"base_score": 85,
},
ViolationType.INTERNAL_MEMO: {
"keywords": [
"internal only",
"internal use only",
"do not distribute",
"not for external",
"office of the ceo",
"organizational priorities",
"growth roadmap",
"strictly confidential",
"internal policy document",
"headcount",
],
"min_matches": 1,
"base_score": 55,
},
}
def _normalize(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def _find_evidence(text: str, keyword: str) -> str | None:
match = re.search(re.escape(keyword.strip()), text, flags=re.IGNORECASE)
if not match:
return None
start = max(0, match.start() - 60)
end = min(len(text), match.end() + 100)
return _normalize(text[start:end])
def _risk_level_from_score(score: int) -> RiskLevel:
if score >= 80:
return RiskLevel.CRITICAL
if score >= 60:
return RiskLevel.HIGH
if score >= 40:
return RiskLevel.MEDIUM
return RiskLevel.LOW
def _action_from_score(score: int) -> ActionClass:
if score >= 80:
return ActionClass.BLOCK
if score >= 40:
return ActionClass.ALERT
return ActionClass.PASS_
def review_corpus(
email_file: str,
subject: str,
sender: str,
recipient: str,
date: str,
body_text: str,
attachment_texts: list[tuple[str, str]],
attachment_results: list[AttachmentResult],
processing_errors: list[str],
) -> DLPResult:
"""Judge an email using DLP_CATEGORIES signals from policy.py."""
# Build full text corpus
parts = [
f"Subject: {subject}",
f"From: {sender}",
f"To: {recipient}",
body_text,
]
for filename, text in attachment_texts:
parts.append(f"Attachment: {filename}")
parts.append(text)
raw = "\n".join(p for p in parts if p)
lower = raw.lower()
evidence_map: dict[ViolationType, list[str]] = defaultdict(list)
score_map: dict[ViolationType, int] = {}
for vtype, rule in _POLICY_KEYWORDS.items():
keywords: list[str] = rule["keywords"]
min_matches: int = rule["min_matches"]
base_score: int = rule["base_score"]
match_count = 0
for kw in keywords:
if kw.lower() in lower:
match_count += 1
ev = _find_evidence(raw, kw)
if ev and ev not in evidence_map[vtype]:
evidence_map[vtype].append(ev)
if match_count < min_matches:
continue
score = base_score + min(12, (match_count - 1) * 3)
# Context boost: external recipient domain
recipient_lower = recipient.lower()
if any(d in recipient_lower for d in ["gmail.com", "yahoo.com", "outlook.com", "hotmail.com"]):
score += 6
score_map[vtype] = min(99, score)
if not score_map:
category_desc = DLP_CATEGORIES # keep reference to show it's used
_ = category_desc # suppress unused warning
return DLPResult(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
risk_level=RiskLevel.LOW,
risk_score=12,
violation_types=[ViolationType.NONE],
action=ActionClass.PASS_,
summary="Policy review found no DLP category signals in this email.",
evidence=[],
attachments=attachment_results,
processing_errors=processing_errors,
)
ranked = sorted(score_map.items(), key=lambda x: x[1], reverse=True)
violation_types = [vt for vt, _ in ranked[:3]]
risk_score = ranked[0][1]
if len(ranked) > 1:
risk_score = min(99, risk_score + min(10, 3 * (len(ranked) - 1)))
evidence: list[str] = []
for vt in violation_types:
evidence.extend(evidence_map[vt][:2])
evidence = evidence[:5]
risk_level = _risk_level_from_score(risk_score)
action = _action_from_score(risk_score)
violation_labels = ", ".join(v.value for v in violation_types)
summary = (
f"Policy review flagged {violation_labels} with {risk_level.value} risk "
f"(score {risk_score}) using DLP_CATEGORIES signals from policy.py."
)
return DLPResult(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
risk_level=risk_level,
risk_score=risk_score,
violation_types=violation_types,
action=action,
summary=summary,
evidence=evidence,
attachments=attachment_results,
processing_errors=processing_errors,
)

310
email_dlp/simulator.py Normal file
View File

@ -0,0 +1,310 @@
"""Deterministic local simulator for DLP analysis."""
from __future__ import annotations
import re
from collections import defaultdict
from .converter import IMAGE_SENTINEL
from .models import ActionClass, AttachmentResult, DLPResult, RiskLevel, ViolationType
_CATEGORY_RULES: dict[ViolationType, dict[str, object]] = {
ViolationType.PII: {
"keywords": [
"personally identifiable information",
" pii",
"employee id",
"account ending",
"direct deposit",
"customer_id",
"first_name",
"last_name",
],
"base_score": 30,
"min_matches": 2,
},
ViolationType.FINANCIAL_DATA: {
"keywords": [
"financial forecast",
"revenue",
"ebitda",
"gross margin",
"margin efficiency",
"sales data",
"annual_sales_usd",
"invoice",
"amount due",
"payment instructions",
"ach",
"budget",
],
"base_score": 42,
},
ViolationType.SOURCE_CODE: {
"keywords": [
"source code",
"api key",
"model weights",
"from __future__ import annotations",
"def ",
"class ",
"@dataclass",
],
"base_score": 88,
"min_matches": 2,
},
ViolationType.REGULATORY_DOCUMENT: {
"keywords": [
"regulatory document",
"regulatory submission",
"cfpb",
"compliance report",
"not for public release",
"draft regulatory",
"prepared by: legal & compliance team",
],
"base_score": 84,
},
ViolationType.LEGAL_CONTRACT: {
"keywords": [
"nondisclosure agreement",
"non-disclosure agreement",
"executed nda",
"disclosing party",
"receiving party",
],
"base_score": 62,
"min_matches": 1,
},
ViolationType.PAYROLL_RECORD: {
"keywords": [
"payroll",
"pay stub",
"compensation record",
"gross:",
"net pay",
"tax deductions",
"pay period",
"direct deposit",
"employee id",
],
"base_score": 90,
},
ViolationType.CUSTOMER_LIST: {
"keywords": [
"customer list",
"prospects",
"crm export",
"raw export",
"customer_id",
"company_name",
"annual_sales_usd",
"top-tier prospects",
],
"base_score": 86,
"min_matches": 2,
},
ViolationType.INTERNAL_MEMO: {
"keywords": [
"internal use only",
"internal memo",
"do not distribute externally",
"office of the ceo",
"organizational priorities",
"growth roadmap",
"internal policy document",
"not for public distribution",
"strictly confidential",
],
"base_score": 52,
"min_matches": 1,
},
}
_RISK_LEVELS = [
(80, RiskLevel.CRITICAL),
(60, RiskLevel.HIGH),
(40, RiskLevel.MEDIUM),
(0, RiskLevel.LOW),
]
def _normalize_text(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def _build_corpus(
subject: str,
sender: str,
recipient: str,
body_text: str,
attachment_texts: list[tuple[str, str]],
) -> tuple[str, str]:
text_chunks = [
f"Subject: {subject}",
f"From: {sender}",
f"To: {recipient}",
body_text,
]
for filename, text in attachment_texts:
text_chunks.append(f"Attachment: {filename}")
# Skip binary image data — base64 payloads produce false keyword matches
if not text.startswith(IMAGE_SENTINEL):
text_chunks.append(text)
raw = "\n".join(chunk for chunk in text_chunks if chunk)
return raw, raw.lower()
def _find_evidence(text: str, keyword: str) -> str | None:
pattern = re.escape(keyword.strip())
match = re.search(pattern, text, flags=re.IGNORECASE)
if not match:
return None
start = max(0, match.start() - 60)
end = min(len(text), match.end() + 100)
return _normalize_text(text[start:end])
def _collect_matches(
raw_text: str,
lower_text: str,
) -> tuple[dict[ViolationType, list[str]], dict[ViolationType, int]]:
evidence_map: dict[ViolationType, list[str]] = defaultdict(list)
score_map: dict[ViolationType, int] = {}
for violation_type, rule in _CATEGORY_RULES.items():
keywords = rule["keywords"]
base_score = int(rule["base_score"])
min_matches = int(rule.get("min_matches", 1))
match_count = 0
for keyword in keywords:
# Use word boundaries to avoid substring false positives (e.g. "ach" in "attached")
pattern = r"\b" + re.escape(keyword) + r"\b"
if re.search(pattern, lower_text):
match_count += 1
evidence = _find_evidence(raw_text, keyword)
if evidence and evidence not in evidence_map[violation_type]:
evidence_map[violation_type].append(evidence)
if match_count < min_matches:
continue
score = base_score + min(12, (match_count - 1) * 4)
score_map[violation_type] = min(score, 99)
return evidence_map, score_map
def _apply_context_boosts(
subject: str,
recipient: str,
attachment_texts: list[tuple[str, str]],
score_map: dict[ViolationType, int],
) -> None:
subject_lower = subject.lower()
recipient_lower = recipient.lower()
if any(domain in recipient_lower for domain in ["gmail.com", "yahoo.com", "outlook.com", "hotmail.com"]):
for violation_type in list(score_map):
score_map[violation_type] = min(99, score_map[violation_type] + 6)
if "urgent" in subject_lower or "confidential" in subject_lower:
for violation_type in list(score_map):
score_map[violation_type] = min(99, score_map[violation_type] + 2)
attachment_names = " ".join(filename.lower() for filename, _ in attachment_texts)
if ".csv" in attachment_names and ViolationType.CUSTOMER_LIST in score_map:
score_map[ViolationType.CUSTOMER_LIST] = min(
99, score_map[ViolationType.CUSTOMER_LIST] + 6
)
if ".py" in attachment_names and ViolationType.SOURCE_CODE in score_map:
score_map[ViolationType.SOURCE_CODE] = min(
99, score_map[ViolationType.SOURCE_CODE] + 4
)
def _risk_level_from_score(risk_score: int) -> RiskLevel:
for threshold, risk_level in _RISK_LEVELS:
if risk_score >= threshold:
return risk_level
return RiskLevel.LOW
def _action_from_score(risk_score: int) -> ActionClass:
if risk_score >= 80:
return ActionClass.BLOCK
if risk_score >= 40:
return ActionClass.ALERT
return ActionClass.PASS_
def _build_summary(
violation_types: list[ViolationType],
risk_level: RiskLevel,
risk_score: int,
) -> str:
if violation_types == [ViolationType.NONE]:
return "No strong DLP indicators were found in the email body or converted attachments."
labels = ", ".join(v.value for v in violation_types)
return (
f"Simulated DLP review flagged {labels} with {risk_level.value} risk "
f"(score {risk_score}) based on the email body and extracted attachment content."
)
def simulate_analysis(
email_file: str,
subject: str,
sender: str,
recipient: str,
date: str,
body_text: str,
attachment_texts: list[tuple[str, str]],
attachment_results: list[AttachmentResult],
processing_errors: list[str],
) -> DLPResult:
"""Predict a DLP result locally without calling an LLM."""
raw_text, lower_text = _build_corpus(
subject=subject,
sender=sender,
recipient=recipient,
body_text=body_text,
attachment_texts=attachment_texts,
)
evidence_map, score_map = _collect_matches(raw_text, lower_text)
_apply_context_boosts(subject, recipient, attachment_texts, score_map)
if not score_map:
violation_types = [ViolationType.NONE]
risk_score = 18
evidence: list[str] = []
else:
ranked = sorted(score_map.items(), key=lambda item: item[1], reverse=True)
violation_types = [violation for violation, _ in ranked[:3]]
risk_score = ranked[0][1]
if len(ranked) > 1:
risk_score = min(99, risk_score + min(10, 3 * (len(ranked) - 1)))
evidence = []
for violation_type in violation_types:
evidence.extend(evidence_map.get(violation_type, [])[:2])
evidence = evidence[:5]
risk_level = _risk_level_from_score(risk_score)
action = _action_from_score(risk_score)
return DLPResult(
email_file=email_file,
subject=subject,
sender=sender,
recipient=recipient,
date=date,
risk_level=risk_level,
risk_score=risk_score,
violation_types=violation_types,
action=action,
summary=_build_summary(violation_types, risk_level, risk_score),
evidence=evidence,
attachments=attachment_results,
processing_errors=processing_errors,
)

26
pyproject.toml Normal file
View File

@ -0,0 +1,26 @@
[project]
name = "email-dlp"
version = "0.1.0"
description = "Email DLP proof-of-concept using local LLM"
requires-python = ">=3.11"
dependencies = [
"markitdown[pdf]>=0.1.0",
"py7zr>=0.21.0",
"openai>=1.30.0",
"pydantic>=2.0",
"beautifulsoup4>=4.12",
"rich>=13.0",
"typer>=0.12",
"python-dotenv>=1.0.0",
"pymupdf>=1.27.2",
]
[project.scripts]
email-dlp = "email_dlp.cli:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["email_dlp"]

1611
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff