V1.0: Class 1 fax — real-world 4-page send to external number confirmed

Core features:
- Class 1 T.30 protocol: full send/receive implementation
- HDLC: DLE-stuffing, FCS strip, USR5637 bit-reversal handling
- T.4 MH encoder/decoder (1728px A4 standard)
- Document pipeline: PDF (Ghostscript), PNG, TIFF input
- Width clamping: US Letter 1734px → 1728px fax standard
- Cover page: CJK rasterization (TW/CN/JP/EN), TIFF + HTML output
- OCR verification: Tesseract 5 with eng+chi_tra, CJK space-tolerant
- API server (axum): health, send, jobs, cover, retry, cancel
- Background worker: auto-poll queue, speed fallback, retry policy
- Modem detection, pool management

Real-world test results (2026-07-23):
- V90 → 25153038: 4 pages, V.17 12000 bps, 2:33 ✅
- USR5637 → 25153038: 4 pages, V.17 12000 bps, 2:26 ✅
- Both faxes confirmed received on remote machine

Tested: loopback (100% pixel match), multi-page, all input formats,
cover pages, OCR verify, API endpoints, worker processing.
13 unit tests pass, 0 new clippy warnings.
This commit is contained in:
Warren
2026-07-24 18:47:15 +08:00
parent d1e92b32fb
commit 55bca92691
155 changed files with 25024 additions and 916 deletions
+608
View File
@@ -0,0 +1,608 @@
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
routing::{delete, get, post, put},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;
use crate::address_book::{Contact, ContactId, Group, GroupId, AddressBookStore, CreateContactRequest, UpdateContactRequest, CreateGroupRequest, UpdateGroupRequest};
#[derive(Clone)]
pub struct AddressBookState {
pub store: Arc<Mutex<AddressBookStore>>,
}
#[derive(Serialize)]
pub struct ContactResponse {
pub id: String,
pub name: String,
pub fax_number: String,
pub company: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
pub category: Option<String>,
pub notes: Option<String>,
pub is_favorite: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<Contact> for ContactResponse {
fn from(contact: Contact) -> Self {
Self {
id: contact.id.to_string(),
name: contact.name,
fax_number: contact.fax_number,
company: contact.company,
email: contact.email,
phone: contact.phone,
category: contact.category,
notes: contact.notes,
is_favorite: contact.is_favorite,
created_at: contact.created_at.to_rfc3339(),
updated_at: contact.updated_at.to_rfc3339(),
}
}
}
#[derive(Serialize)]
pub struct ContactListResponse {
pub contacts: Vec<ContactResponse>,
pub total: usize,
}
#[derive(Serialize)]
pub struct GroupResponse {
pub id: String,
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl From<Group> for GroupResponse {
fn from(group: Group) -> Self {
Self {
id: group.id.to_string(),
name: group.name,
description: group.description,
color: group.color,
created_at: group.created_at.to_rfc3339(),
updated_at: group.updated_at.to_rfc3339(),
}
}
}
#[derive(Serialize)]
pub struct GroupListResponse {
pub groups: Vec<GroupResponse>,
pub total: usize,
}
#[derive(Deserialize)]
pub struct SearchQuery {
pub query: Option<String>,
pub category: Option<String>,
}
async fn create_contact(
State(state): State<AddressBookState>,
Json(req): Json<CreateContactRequest>,
) -> Result<Json<ContactResponse>, (StatusCode, Json<ErrorResponse>)> {
let mut contact = Contact::new(req.name, req.fax_number);
contact.company = req.company;
contact.email = req.email;
contact.phone = req.phone;
contact.category = req.category;
contact.notes = req.notes;
let store = state.store.lock().await;
store.create_contact(&contact).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "create_contact_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Created contact: {} - {}", contact.name, contact.fax_number);
Ok(Json(ContactResponse::from(contact)))
}
async fn get_contact(
State(state): State<AddressBookState>,
Path(id): Path<String>,
) -> Result<Json<ContactResponse>, (StatusCode, Json<ErrorResponse>)> {
let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_contact_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
let contact = store.get_contact(&contact_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "get_contact_failed".to_string(),
message: e.to_string(),
}),
)
})?;
match contact {
Some(c) => Ok(Json(ContactResponse::from(c))),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "contact_not_found".to_string(),
message: "Contact not found".to_string(),
}),
)),
}
}
async fn list_contacts(
State(state): State<AddressBookState>,
Query(query): Query<SearchQuery>,
) -> Result<Json<ContactListResponse>, (StatusCode, Json<ErrorResponse>)> {
let store = state.store.lock().await;
let contacts = if let Some(search_query) = query.query {
store.search_contacts(&search_query).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "search_contacts_failed".to_string(),
message: e.to_string(),
}),
)
})?
} else if let Some(category) = query.category {
store.list_contacts(Some(&category)).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "list_contacts_failed".to_string(),
message: e.to_string(),
}),
)
})?
} else {
store.list_contacts(None).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "list_contacts_failed".to_string(),
message: e.to_string(),
}),
)
})?
};
let total = contacts.len();
Ok(Json(ContactListResponse {
contacts: contacts.into_iter().map(ContactResponse::from).collect(),
total,
}))
}
async fn update_contact(
State(state): State<AddressBookState>,
Path(id): Path<String>,
Json(req): Json<UpdateContactRequest>,
) -> Result<Json<ContactResponse>, (StatusCode, Json<ErrorResponse>)> {
let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_contact_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
let mut contact = store.get_contact(&contact_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "get_contact_failed".to_string(),
message: e.to_string(),
}),
)
})?.ok_or((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "contact_not_found".to_string(),
message: "Contact not found".to_string(),
}),
))?;
if let Some(name) = req.name {
contact.name = name;
}
if let Some(fax_number) = req.fax_number {
contact.fax_number = fax_number;
}
if let Some(company) = req.company {
contact.company = Some(company);
}
if let Some(email) = req.email {
contact.email = Some(email);
}
if let Some(phone) = req.phone {
contact.phone = Some(phone);
}
if let Some(category) = req.category {
contact.category = Some(category);
}
if let Some(notes) = req.notes {
contact.notes = Some(notes);
}
if let Some(is_favorite) = req.is_favorite {
contact.is_favorite = is_favorite;
}
contact.update();
store.update_contact(&contact_id, &contact).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "update_contact_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Updated contact: {} - {}", contact.name, contact.fax_number);
Ok(Json(ContactResponse::from(contact)))
}
async fn delete_contact(
State(state): State<AddressBookState>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let contact_id = ContactId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_contact_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
store.delete_contact(&contact_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "delete_contact_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Deleted contact: {}", id);
Ok(StatusCode::NO_CONTENT)
}
async fn create_group(
State(state): State<AddressBookState>,
Json(req): Json<CreateGroupRequest>,
) -> Result<Json<GroupResponse>, (StatusCode, Json<ErrorResponse>)> {
let mut group = Group::new(req.name);
group.description = req.description;
group.color = req.color;
let store = state.store.lock().await;
store.create_group(&group).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "create_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Created group: {}", group.name);
Ok(Json(GroupResponse::from(group)))
}
async fn get_group(
State(state): State<AddressBookState>,
Path(id): Path<String>,
) -> Result<Json<GroupResponse>, (StatusCode, Json<ErrorResponse>)> {
let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
let group = store.get_group(&group_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "get_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
match group {
Some(g) => Ok(Json(GroupResponse::from(g))),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "group_not_found".to_string(),
message: "Group not found".to_string(),
}),
)),
}
}
async fn list_groups(
State(state): State<AddressBookState>,
) -> Result<Json<GroupListResponse>, (StatusCode, Json<ErrorResponse>)> {
let store = state.store.lock().await;
let groups = store.list_groups().map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "list_groups_failed".to_string(),
message: e.to_string(),
}),
)
})?;
let total = groups.len();
Ok(Json(GroupListResponse {
groups: groups.into_iter().map(GroupResponse::from).collect(),
total,
}))
}
async fn update_group(
State(state): State<AddressBookState>,
Path(id): Path<String>,
Json(req): Json<UpdateGroupRequest>,
) -> Result<Json<GroupResponse>, (StatusCode, Json<ErrorResponse>)> {
let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
let mut group = store.get_group(&group_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "get_group_failed".to_string(),
message: e.to_string(),
}),
)
})?.ok_or((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "group_not_found".to_string(),
message: "Group not found".to_string(),
}),
))?;
if let Some(name) = req.name {
group.name = name;
}
if let Some(description) = req.description {
group.description = Some(description);
}
if let Some(color) = req.color {
group.color = Some(color);
}
group.update();
store.update_group(&group_id, &group).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "update_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Updated group: {}", group.name);
Ok(Json(GroupResponse::from(group)))
}
async fn delete_group(
State(state): State<AddressBookState>,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
store.delete_group(&group_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "delete_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Deleted group: {}", id);
Ok(StatusCode::NO_CONTENT)
}
async fn add_contact_to_group(
State(state): State<AddressBookState>,
Path((contact_id, group_id)): Path<(String, String)>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let contact_uuid = ContactId::from_uuid(uuid::Uuid::parse_str(&contact_id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_contact_id".to_string(),
message: e.to_string(),
}),
)
})?);
let group_uuid = GroupId::from_uuid(uuid::Uuid::parse_str(&group_id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
store.add_contact_to_group(&contact_uuid, &group_uuid).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "add_contact_to_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Added contact {} to group {}", contact_id, group_id);
Ok(StatusCode::NO_CONTENT)
}
async fn remove_contact_from_group(
State(state): State<AddressBookState>,
Path((contact_id, group_id)): Path<(String, String)>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
let contact_uuid = ContactId::from_uuid(uuid::Uuid::parse_str(&contact_id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_contact_id".to_string(),
message: e.to_string(),
}),
)
})?);
let group_uuid = GroupId::from_uuid(uuid::Uuid::parse_str(&group_id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
store.remove_contact_from_group(&contact_uuid, &group_uuid).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "remove_contact_from_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
info!("Removed contact {} from group {}", contact_id, group_id);
Ok(StatusCode::NO_CONTENT)
}
async fn get_contacts_in_group(
State(state): State<AddressBookState>,
Path(id): Path<String>,
) -> Result<Json<ContactListResponse>, (StatusCode, Json<ErrorResponse>)> {
let group_id = GroupId::from_uuid(uuid::Uuid::parse_str(&id).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "invalid_group_id".to_string(),
message: e.to_string(),
}),
)
})?);
let store = state.store.lock().await;
let contacts = store.get_contacts_in_group(&group_id).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "get_contacts_in_group_failed".to_string(),
message: e.to_string(),
}),
)
})?;
let total = contacts.len();
Ok(Json(ContactListResponse {
contacts: contacts.into_iter().map(ContactResponse::from).collect(),
total,
}))
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
pub message: String,
}
pub fn address_book_routes(store: Arc<Mutex<AddressBookStore>>) -> Router {
let state = AddressBookState { store };
Router::new()
.route("/contacts", post(create_contact))
.route("/contacts", get(list_contacts))
.route("/contacts/:id", get(get_contact))
.route("/contacts/:id", put(update_contact))
.route("/contacts/:id", delete(delete_contact))
.route("/groups", post(create_group))
.route("/groups", get(list_groups))
.route("/groups/:id", get(get_group))
.route("/groups/:id", put(update_group))
.route("/groups/:id", delete(delete_group))
.route("/groups/:id/contacts", get(get_contacts_in_group))
.route("/contacts/:contact_id/groups/:group_id", post(add_contact_to_group))
.route("/contacts/:contact_id/groups/:group_id", delete(remove_contact_from_group))
.with_state(state)
}