Compare commits

...

19 Commits

Author SHA1 Message Date
2393b2649a docs: add project status tracking document
Current implementation status:
-  Phase 1 Complete: Domain Layer (100%)
- 🚧 Phase 2 In Progress: Application Layer (5%)
-  Phases 3-6 Pending

Detailed tracking:
- All completed entities, value objects, enums, services
- All pending repository interfaces, commands, queries
- All pending infrastructure implementations
- All pending API controllers and workers
- Build status confirmation
- Progress visualization (~15% complete)
- Prioritized next steps for future agents

This document provides quick overview of what's done and what's next.
2026-07-07 19:00:07 +02:00
331b73000e docs: add critical notes and future enhancements for agents
Important notes:
- Database schema must NEVER be modified
- MessageId hash algorithm must match legacy system exactly
- No git commits without explicit permission
- Naming conventions (SNAKE_CASE DB, PascalCase C#)

Future enhancements:
- RabbitMQ queue implementation plan (replacing in-memory queue)
- Complete migration path and configuration examples
- Pending implementation tasks for each phase
- Known issues and limitations (PdfSharp, windream COM)

Architecture decisions:
- Clean Architecture with DDD
- CQRS pattern with MediatR
- Repository pattern

Development guidelines:
- Code style conventions
- Logging with Serilog
- Configuration management
- Error handling strategies
- Deployment scenarios (IIS/Windows Service)
2026-07-07 18:59:57 +02:00
e789afe26a docs: add comprehensive implementation guide for AI agents
Step-by-step guide covering all remaining phases:
- Phase 2: Application Layer (Repositories, Services, Commands, Queries, Validators)
- Phase 3: Infrastructure Layer (DbContext, Repositories, External Services)
- Phase 4: API Layer (Controllers, Workers, Middleware)
- Phase 5: Configuration (appsettings, Serilog, Scalar)
- Phase 6: Testing (Unit tests, Integration tests)
- Phase 7: Documentation (README.md in German)
- Phase 8: Build and deployment

Includes complete code examples, best practices, and verification steps.
Future AI agents can follow this guide to continue development systematically.
2026-07-07 18:59:46 +02:00
dd04cd6cba docs: add legacy system analysis documentation
Comprehensive analysis of the VB.NET legacy system:
- Complete database schema documentation
- All table structures (TBDD_*, TBEMLP_* tables)
- Legacy business logic analysis
- VB.NET code patterns and conventions
- Migration considerations

This documentation ensures new implementation maintains compatibility
with existing database and business rules.
2026-07-07 18:59:37 +02:00
43101a6e61 feat(application): add DTOs and application layer dependencies
DTOs:
- EmailProfileDto: Profile data transfer object
- EmailAccountDto: Email account data transfer object
- EmailHistoryDto: Email history data transfer object
- EmailAttachmentDto: Attachment data transfer object

Dependencies:
- MediatR 14.2.0 for CQRS (Commands/Queries)
- AutoMapper 12.0.1 for entity-DTO mapping
- FluentValidation 12.1.1 for input validation

This provides the foundation for the application layer implementation.
2026-07-07 18:59:31 +02:00
146b56ff85 build(domain): add MediatR dependency for domain events
- Add MediatR 12.2.0 package for event-driven architecture
- Enables domain events like EmailProcessedEvent
2026-07-07 18:59:22 +02:00
721603bb47 feat(domain): add domain services and events
- MessageIdGenerator: Generates unique message IDs using legacy-compatible SHA256 hash
  Algorithm matches VB.NET system for duplicate detection
- EmailProcessedEvent: MediatR domain event for email processing completion

Domain services encapsulate business logic that doesn't belong to entities.
2026-07-07 18:59:17 +02:00
c97073775b feat(domain): add all domain entities with legacy database mapping
Entities mapped to legacy database tables using [Table] and [Column] attributes:
- EmailAccount → TBDD_EMAIL_ACCOUNT (OAuth2 and password auth support)
- EmailProfile → TBEMLP_POLL_PROFILES (polling configuration)
- EmailProcess → TBEMLP_POLL_PROCESS (process definitions)
- ProcessStep → TBEMLP_POLL_STEPS (indexing steps)
- IndexingStep → TBEMLP_POLL_INDEXING_STEPS (DMS indexing fields)
- EmailHistory → TBEMLP_HISTORY (processed emails)
- EmailAttachment → TBEMLP_HISTORY_ATTACHMENT (email attachments)
- EmailOutbox → TBEMLP_EMAIL_OUT (outgoing email queue)

All entities follow Clean Architecture and DDD principles.
Database schema is read-only - no migrations will modify existing tables.
SNAKE_CASE columns mapped to PascalCase properties.
2026-07-07 18:59:10 +02:00
18bb07cd93 feat(domain): add domain exception hierarchy
- DomainException: Base exception for all domain errors
- ValidationException: Business rule validation failures
- AttachmentProcessingException: Attachment-specific processing errors

Exceptions maintain error code compatibility with legacy system.
2026-07-07 18:58:59 +02:00
f6946d812a feat(domain): add value objects for email domain
- MessageId: Unique message identifier with SHA256 hash
  Uses same algorithm as legacy system for duplicate detection compatibility
  Hash format: SHA256({originalMessageId}|{sender}|{date}|{subject})
- EmailAddress: Email address validation and parsing with name support

Value objects ensure immutability and value-based equality.
2026-07-07 18:58:54 +02:00
6098112bb4 feat(domain): add domain enumerations
- ErrorCode: All error codes from legacy system (10001-10010)
- ProcessType: Email process types (ProcessManager, AttachmentSniffer, ZugFeRDParser)
- AuthenticationType: Authentication methods (UsernamePassword, OAuth2)
- EmailStatus: Email processing status tracking
- AttachmentStatus: Attachment validation status

These enums maintain compatibility with the legacy VB.NET system.
2026-07-07 18:58:18 +02:00
05a36e8045 feat(domain): add common base classes and interfaces
- Add BaseEntity with audit fields (CreatedDate, CreatedBy, ModifiedDate, ModifiedBy)
- Add IAggregateRoot marker interface for DDD aggregate roots
- Add ValueObject base class with equality comparison by value

These classes provide the foundation for all domain entities and value objects.
2026-07-07 18:58:10 +02:00
915d01fc03 Add project references and fix encoding issue in tests
Added project references to establish dependencies between
the API, Application, Domain, and Infrastructure projects.
Updated `DigitalData.EmailProfiler.Tests.csproj` to include
references to all layers for testing purposes. Fixed a BOM
encoding issue in the test project file. Added xUnit usage
directive to ensure proper test framework integration.
2026-07-07 14:38:55 +02:00
a88702d9e2 Update .gitignore to exclude specific files and paths
Updated the .gitignore file to ignore the following files and directories:
- `FodyWeavers.xsd`
- `/EnvelopeGenerator.Tests.Application/annotations.json`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output`
- `/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md+/legacy/App`

These changes ensure that unnecessary or sensitive files are excluded from version control.
2026-07-07 13:36:37 +02:00
1144f58ebb Add DigitalData.EmailProfiler.Tests project
A new test project, `DigitalData.EmailProfiler.Tests`, has been added to the solution. The project is configured as a .NET 8.0 test project with xUnit as the testing framework. It includes necessary NuGet dependencies such as `coverlet.collector`, `Microsoft.NET.Test.Sdk`, and `xunit.runner.visualstudio`.

The solution file has been updated to include the new project, along with its build configurations (`Debug|Any CPU` and `Release|Any CPU`). A new solution folder, `tests`, has been added, and the test project is nested under it.
2026-07-07 13:34:08 +02:00
690aee02dd Update solution and add new projects targeting .NET 8.0
Updated Visual Studio version in the solution file to 17.14.36717.8.
Added three new projects: Infrastructure, Domain, and Application,
all targeting .NET 8.0. Enabled implicit global usings and nullable
reference types in the new projects. Updated solution configuration
and nested the new projects under the `src` folder.
2026-07-07 13:31:56 +02:00
f3552dbdaa Add background service and update project configuration
Added a `Worker` class as a hosted background service to log
periodic messages. Updated `DigitalData.EmailProfiler.API.csproj`
to include `UserSecretsId` for secure development storage and
added `Microsoft.Extensions.Hosting` package. Replaced the
`Controllers` folder reference with `Properties`. Updated
`Program.cs` to register the `Worker` service, enable API
exploration, and retain Swagger configuration.
2026-07-07 13:25:58 +02:00
8a19a8a8bb init API 2026-07-07 13:18:24 +02:00
34baa6fbd9 Update .gitattributes and .gitignore for repo consistency
Improved repository configuration by updating `.gitattributes` to:
- Normalize line endings automatically.
- Define diff behavior for C# files and common document formats.
- Add optional merge driver settings for Visual Studio project files.
- Treat image files as binary.

Enhanced `.gitignore` to:
- Exclude Visual Studio-specific files, build outputs, and temporary files.
- Ignore files generated by add-ons, testing frameworks, and tools.
- Add project-specific exclusions for `EnvelopeGenerator`.

These changes enhance maintainability, reduce clutter, and prevent unnecessary files from being committed.
2026-07-07 13:18:15 +02:00
41 changed files with 3350 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

372
.gitignore vendored Normal file
View File

@@ -0,0 +1,372 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/EnvelopeGenerator.Web/.config/dotnet-tools.json
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
/EnvelopeGenerator.Tests.Application/annotations.json
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/legacy/App

View File

@@ -0,0 +1,60 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36717.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.EmailProfiler.API", "src\DigitalData.EmailProfiler.API\DigitalData.EmailProfiler.API.csproj", "{456817AB-67A3-49DD-9EC5-1253A727B958}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.EmailProfiler.Infrastructure", "src\DigitalData.EmailProfiler.Infrastructure\DigitalData.EmailProfiler.Infrastructure.csproj", "{D2C32417-FE92-4063-97A9-3DA8D811E561}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.EmailProfiler.Domain", "src\DigitalData.EmailProfiler.Domain\DigitalData.EmailProfiler.Domain.csproj", "{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.EmailProfiler.Application", "src\DigitalData.EmailProfiler.Application\DigitalData.EmailProfiler.Application.csproj", "{1874A827-C6A5-EB5E-0FE9-30A7200382B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.EmailProfiler.Tests", "tests\DigitalData.EmailProfiler.Tests\DigitalData.EmailProfiler.Tests.csproj", "{211FB65F-2406-474E-A426-DA246B250AB8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4F20FEFD-9289-42C6-ABA6-8DB236D74559}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{456817AB-67A3-49DD-9EC5-1253A727B958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Debug|Any CPU.Build.0 = Debug|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Release|Any CPU.ActiveCfg = Release|Any CPU
{456817AB-67A3-49DD-9EC5-1253A727B958}.Release|Any CPU.Build.0 = Release|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2C32417-FE92-4063-97A9-3DA8D811E561}.Release|Any CPU.Build.0 = Release|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043}.Release|Any CPU.Build.0 = Release|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1874A827-C6A5-EB5E-0FE9-30A7200382B7}.Release|Any CPU.Build.0 = Release|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211FB65F-2406-474E-A426-DA246B250AB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{456817AB-67A3-49DD-9EC5-1253A727B958} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{D2C32417-FE92-4063-97A9-3DA8D811E561} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{76ADC1D0-4DFA-0B1E-57C9-2636434A0043} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{1874A827-C6A5-EB5E-0FE9-30A7200382B7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{211FB65F-2406-474E-A426-DA246B250AB8} = {4F20FEFD-9289-42C6-ABA6-8DB236D74559}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {90E29FDC-F6C6-414F-94BF-25DF61D18060}
EndGlobalSection
EndGlobal

954
IMPLEMENTATION_GUIDE.md Normal file
View File

@@ -0,0 +1,954 @@
# EmailProfiler - Implementation Guide for AI Agents
## Overview
This guide provides step-by-step instructions for AI agents to continue the implementation of the EmailProfiler application. The project is a modern .NET 8.0 rewrite of a legacy VB.NET email automation system.
---
## Current Status (2026-07-07)
**COMPLETED**:
- Domain Layer (100%)
- All entities with proper `[Table]` and `[Column]` attributes
- Value Objects (MessageId, EmailAddress)
- Enums (ErrorCode, ProcessType, etc.)
- Domain Services (MessageIdGenerator)
- Domain Events (EmailProcessedEvent)
- Exceptions (DomainException, ValidationException, AttachmentProcessingException)
- agents.md documentation
- Project builds successfully
🚧 **IN PROGRESS**:
- Application Layer (5% - only DTOs created)
**PENDING**:
- Application Layer (95%)
- Infrastructure Layer (0%)
- API Layer (minimal structure only)
- Testing (0%)
- README.md documentation (0%)
---
## Architecture Overview
```
DigitalData.EmailProfiler/
├── src/
│ ├── Domain/ ✅ COMPLETE
│ ├── Application/ 🚧 IN PROGRESS (5%)
│ ├── Infrastructure/ ❌ TODO
│ └── API/ ❌ TODO (minimal structure exists)
├── tests/
│ └── Tests/ ❌ TODO
├── legacy/ 📖 Reference only
├── agents.md ✅ COMPLETE
├── README.md ❌ TODO
└── IMPLEMENTATION_GUIDE.md 📄 This file
```
---
## Phase-by-Phase Implementation Plan
### PHASE 2: Application Layer (Current Focus)
#### 2.1. Create Repository Interfaces
**Location**: `src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/`
Create these files:
**IEmailProfileRepository.cs**:
```csharp
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
public interface IEmailProfileRepository
{
Task<EmailProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<List<EmailProfile>> GetAllAsync(CancellationToken cancellationToken = default);
Task<List<EmailProfile>> GetActiveProfilesAsync(CancellationToken cancellationToken = default);
Task<List<EmailProfile>> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default);
Task<int> AddAsync(EmailProfile profile, CancellationToken cancellationToken = default);
Task UpdateAsync(EmailProfile profile, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}
```
**IEmailAccountRepository.cs**:
```csharp
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
public interface IEmailAccountRepository
{
Task<EmailAccount?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<List<EmailAccount>> GetAllAsync(CancellationToken cancellationToken = default);
Task<List<EmailAccount>> GetActiveAccountsAsync(CancellationToken cancellationToken = default);
Task<int> AddAsync(EmailAccount account, CancellationToken cancellationToken = default);
Task UpdateAsync(EmailAccount account, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}
```
**IEmailHistoryRepository.cs**:
```csharp
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
public interface IEmailHistoryRepository
{
Task<EmailHistory?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<EmailHistory?> GetByMessageIdHashAsync(string hash, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(string messageIdHash, CancellationToken cancellationToken = default);
Task<List<EmailHistory>> GetByProfileIdAsync(int profileId, DateTime? from, DateTime? to, CancellationToken cancellationToken = default);
Task<int> AddAsync(EmailHistory history, CancellationToken cancellationToken = default);
Task UpdateAsync(EmailHistory history, CancellationToken cancellationToken = default);
}
```
**IEmailProcessRepository.cs**, **IEmailOutboxRepository.cs** - Similar patterns.
#### 2.2. Create Service Interfaces
**Location**: `src/DigitalData.EmailProfiler.Application/Interfaces/Services/`
**IEmailService.cs**:
```csharp
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
public interface IEmailService
{
Task<List<EmailMessage>> FetchUnreadEmailsAsync(
EmailAccount account,
CancellationToken cancellationToken = default);
Task<bool> TestConnectionAsync(
EmailAccount account,
CancellationToken cancellationToken = default);
Task SendEmailAsync(
EmailAccount account,
string recipient,
string subject,
string body,
bool isHtml = true,
CancellationToken cancellationToken = default);
Task DeleteEmailAsync(EmailAccount account, int imapUid, CancellationToken cancellationToken = default);
Task MoveEmailAsync(EmailAccount account, int imapUid, string folderName, CancellationToken cancellationToken = default);
}
public class EmailMessage
{
public int ImapUid { get; set; }
public string MessageId { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public DateTime Date { get; set; }
public string BodyHtml { get; set; } = string.Empty;
public string BodyText { get; set; } = string.Empty;
public List<EmailAttachmentData> Attachments { get; set; } = new();
public byte[] RawEmailData { get; set; } = Array.Empty<byte>();
}
public class EmailAttachmentData
{
public string FileName { get; set; } = string.Empty;
public string ContentType { get; set; } = string.Empty;
public byte[] Data { get; set; } = Array.Empty<byte>();
}
```
**IPdfProcessingService.cs**, **IDmsService.cs**, **IEncryptionService.cs**, **IEmailQueue.cs** - See agents.md for examples.
#### 2.3. Create MediatR Commands
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Commands/`
**CreateEmailProfileCommand.cs**:
```csharp
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
public record CreateEmailProfileCommand(
string ProfileName,
int EmailAccountId,
int? ProcessId,
int PollIntervalMinutes) : IRequest<int>;
public class CreateEmailProfileCommandHandler : IRequestHandler<CreateEmailProfileCommand, int>
{
private readonly IEmailProfileRepository _repository;
public CreateEmailProfileCommandHandler(IEmailProfileRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
{
var profile = new EmailProfile
{
ProfileName = request.ProfileName,
EmailAccountId = request.EmailAccountId,
ProcessId = request.ProcessId,
PollIntervalMinutes = request.PollIntervalMinutes,
IsActive = true,
AddedWhen = DateTime.UtcNow
};
return await _repository.AddAsync(profile, cancellationToken);
}
}
```
**UpdateEmailProfileCommand.cs**, **DeleteEmailProfileCommand.cs**, **ActivateProfileCommand.cs** - Similar patterns.
#### 2.4. Create MediatR Queries
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Queries/`
**GetEmailProfilesQuery.cs**:
```csharp
using MediatR;
using AutoMapper;
using DigitalData.EmailProfiler.Application.Common.Dtos;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
public record GetEmailProfilesQuery : IRequest<List<EmailProfileDto>>;
public class GetEmailProfilesQueryHandler : IRequestHandler<GetEmailProfilesQuery, List<EmailProfileDto>>
{
private readonly IEmailProfileRepository _repository;
private readonly IMapper _mapper;
public GetEmailProfilesQueryHandler(IEmailProfileRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<EmailProfileDto>> Handle(GetEmailProfilesQuery request, CancellationToken cancellationToken)
{
var profiles = await _repository.GetAllAsync(cancellationToken);
return _mapper.Map<List<EmailProfileDto>>(profiles);
}
}
```
**GetEmailProfileByIdQuery.cs**, **GetActiveProfilesQuery.cs**, **GetProfilesDueForPollingQuery.cs** - Similar patterns.
#### 2.5. Create Validators
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Validators/`
**CreateEmailProfileCommandValidator.cs**:
```csharp
using FluentValidation;
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Validators;
public class CreateEmailProfileCommandValidator : AbstractValidator<CreateEmailProfileCommand>
{
public CreateEmailProfileCommandValidator()
{
RuleFor(x => x.ProfileName)
.NotEmpty().WithMessage("Profile name is required")
.MaximumLength(100).WithMessage("Profile name must not exceed 100 characters");
RuleFor(x => x.EmailAccountId)
.GreaterThan(0).WithMessage("Email account ID must be greater than 0");
RuleFor(x => x.PollIntervalMinutes)
.GreaterThan(0).WithMessage("Poll interval must be greater than 0")
.LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)");
}
}
```
#### 2.6. Create AutoMapper Profiles
**Location**: `src/DigitalData.EmailProfiler.Application/Common/Mappings/`
**MappingProfile.cs**:
```csharp
using AutoMapper;
using DigitalData.EmailProfiler.Domain.Entities;
using DigitalData.EmailProfiler.Application.Common.Dtos;
namespace DigitalData.EmailProfiler.Application.Common.Mappings;
public class MappingProfile : Profile
{
public MappingProfile()
{
// EmailProfile mappings
CreateMap<EmailProfile, EmailProfileDto>()
.ForMember(d => d.EmailAccountName, opt => opt.MapFrom(s => s.EmailAccount != null ? s.EmailAccount.AccountName : null))
.ForMember(d => d.ProcessName, opt => opt.MapFrom(s => s.EmailProcess != null ? s.EmailProcess.ProcessName : null));
// EmailAccount mappings
CreateMap<EmailAccount, EmailAccountDto>();
// EmailHistory mappings
CreateMap<EmailHistory, EmailHistoryDto>()
.ForMember(d => d.ProfileName, opt => opt.MapFrom(s => s.Profile != null ? s.Profile.ProfileName : null))
.ForMember(d => d.Attachments, opt => opt.MapFrom(s => s.Attachments));
// EmailAttachment mappings
CreateMap<EmailAttachment, EmailAttachmentDto>();
}
}
```
#### 2.7. Create DependencyInjection.cs
**Location**: `src/DigitalData.EmailProfiler.Application/DependencyInjection.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using FluentValidation;
using System.Reflection;
namespace DigitalData.EmailProfiler.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
// MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
// AutoMapper
services.AddAutoMapper(assembly);
// FluentValidation
services.AddValidatorsFromAssembly(assembly);
return services;
}
}
```
---
### PHASE 3: Infrastructure Layer
#### 3.1. Add NuGet Packages
```bash
cd src/DigitalData.EmailProfiler.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package MailKit
dotnet add package MimeKit
dotnet add package PdfSharp
dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.AspNetCore.DataProtection
```
#### 3.2. Create DbContext
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/Persistence/EmailProfilerDbContext.cs`
```csharp
using Microsoft.EntityFrameworkCore;
using DigitalData.EmailProfiler.Domain.Entities;
using System.Reflection;
namespace DigitalData.EmailProfiler.Infrastructure.Persistence;
public class EmailProfilerDbContext : DbContext
{
public EmailProfilerDbContext(DbContextOptions<EmailProfilerDbContext> options) : base(options) { }
public DbSet<EmailAccount> EmailAccounts { get; set; }
public DbSet<EmailProfile> EmailProfiles { get; set; }
public DbSet<EmailProcess> EmailProcesses { get; set; }
public DbSet<ProcessStep> ProcessSteps { get; set; }
public DbSet<IndexingStep> IndexingSteps { get; set; }
public DbSet<EmailHistory> EmailHistories { get; set; }
public DbSet<EmailAttachment> EmailAttachments { get; set; }
public DbSet<EmailOutbox> EmailOutbox { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply configurations from assembly (if you create IEntityTypeConfiguration classes)
// modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
// Note: All entity configurations are already done via attributes in Domain entities
// This is important - DO NOT modify database schema here!
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Auto-populate audit fields
var entries = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedDate = DateTime.UtcNow;
entry.Entity.CreatedBy = "System"; // TODO: Get from current user context
}
if (entry.State == EntityState.Modified)
{
entry.Entity.ModifiedDate = DateTime.UtcNow;
entry.Entity.ModifiedBy = "System"; // TODO: Get from current user context
}
}
return base.SaveChangesAsync(cancellationToken);
}
}
```
#### 3.3. Create Repositories
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/Persistence/Repositories/`
**EmailProfileRepository.cs**:
```csharp
using Microsoft.EntityFrameworkCore;
using DigitalData.EmailProfiler.Domain.Entities;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
namespace DigitalData.EmailProfiler.Infrastructure.Persistence.Repositories;
public class EmailProfileRepository : IEmailProfileRepository
{
private readonly EmailProfilerDbContext _context;
public EmailProfileRepository(EmailProfilerDbContext context)
{
_context = context;
}
public async Task<EmailProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.EmailProfiles
.Include(p => p.EmailAccount)
.Include(p => p.EmailProcess)
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
}
public async Task<List<EmailProfile>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.EmailProfiles
.Include(p => p.EmailAccount)
.Include(p => p.EmailProcess)
.OrderBy(p => p.Sequence)
.ToListAsync(cancellationToken);
}
public async Task<List<EmailProfile>> GetActiveProfilesAsync(CancellationToken cancellationToken = default)
{
return await _context.EmailProfiles
.Include(p => p.EmailAccount)
.Include(p => p.EmailProcess)
.Where(p => p.IsActive && p.EmailAccount!.IsActive)
.OrderBy(p => p.Sequence)
.ToListAsync(cancellationToken);
}
public async Task<List<EmailProfile>> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
return await _context.EmailProfiles
.Include(p => p.EmailAccount)
.Include(p => p.EmailProcess)
.Where(p => p.IsActive
&& p.EmailAccount!.IsActive
&& (!p.LastPollTime.HasValue ||
EF.Functions.DateDiffMinute(p.LastPollTime.Value, now) >= p.PollIntervalMinutes))
.OrderBy(p => p.Sequence)
.ToListAsync(cancellationToken);
}
public async Task<int> AddAsync(EmailProfile profile, CancellationToken cancellationToken = default)
{
_context.EmailProfiles.Add(profile);
await _context.SaveChangesAsync(cancellationToken);
return profile.Id;
}
public async Task UpdateAsync(EmailProfile profile, CancellationToken cancellationToken = default)
{
_context.EmailProfiles.Update(profile);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
{
var profile = await GetByIdAsync(id, cancellationToken);
if (profile != null)
{
_context.EmailProfiles.Remove(profile);
await _context.SaveChangesAsync(cancellationToken);
}
}
}
```
Create similar repositories for **EmailAccountRepository**, **EmailHistoryRepository**, etc.
#### 3.4. Create External Services
**MailKitEmailService.cs**, **PdfSharpProcessingService.cs**, **WindreamDmsService.cs**, **EncryptionService.cs**, **InMemoryEmailQueue.cs**
(See agents.md for examples - these are complex services)
#### 3.5. Create DependencyInjection.cs
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/DependencyInjection.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using DigitalData.EmailProfiler.Infrastructure.Persistence;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using DigitalData.EmailProfiler.Infrastructure.Persistence.Repositories;
namespace DigitalData.EmailProfiler.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// DbContext
services.AddDbContext<EmailProfilerDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
sqlOptions.CommandTimeout(60);
}));
// Repositories
services.AddScoped<IEmailProfileRepository, EmailProfileRepository>();
services.AddScoped<IEmailAccountRepository, EmailAccountRepository>();
services.AddScoped<IEmailHistoryRepository, EmailHistoryRepository>();
// ... add other repositories
// External Services
// services.AddScoped<IEmailService, MailKitEmailService>();
// services.AddScoped<IPdfProcessingService, PdfSharpProcessingService>();
// services.AddScoped<IDmsService, WindreamDmsService>();
// services.AddScoped<IEncryptionService, DataProtectionEncryptionService>();
// services.AddSingleton<IEmailQueue, InMemoryEmailQueue>();
return services;
}
}
```
---
### PHASE 4: API Layer
#### 4.1. Add NuGet Packages
```bash
cd src/DigitalData.EmailProfiler.API
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.MSSqlServer
dotnet add package Scalar.AspNetCore
```
#### 4.2. Update Program.cs
**Location**: `src/DigitalData.EmailProfiler.API/Program.cs`
```csharp
using DigitalData.EmailProfiler.API;
using DigitalData.EmailProfiler.Application;
using DigitalData.EmailProfiler.Infrastructure;
using Serilog;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File("logs/emailprofiler-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
builder.Host.UseSerilog();
// Check for Windows Service mode
if (builder.Configuration["Hosting:Mode"] == "WindowsService")
{
builder.Host.UseWindowsService();
}
// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add Application and Infrastructure layers
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
// Add Background Workers
// builder.Services.AddHostedService<EmailPollingWorker>();
// builder.Services.AddHostedService<EmailSenderWorker>();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
// Add Scalar
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
try
{
Log.Information("Starting EmailProfiler API");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
```
#### 4.3. Create Controllers
**Location**: `src/DigitalData.EmailProfiler.API/Controllers/`
**EmailProfilesController.cs**:
```csharp
using Microsoft.AspNetCore.Mvc;
using MediatR;
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
using DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
namespace DigitalData.EmailProfiler.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class EmailProfilesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<EmailProfilesController> _logger;
public EmailProfilesController(IMediator mediator, ILogger<EmailProfilesController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
{
var query = new GetEmailProfilesQuery();
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
var query = new GetEmailProfileByIdQuery(id);
var result = await _mediator.Send(query, cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateEmailProfileCommand command, CancellationToken cancellationToken)
{
var id = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetById), new { id }, id);
}
// Add Update, Delete, Activate, Deactivate endpoints
}
```
Create similar controllers for **EmailAccountsController**, **EmailHistoryController**, **DashboardController**.
#### 4.4. Create Background Workers
**Location**: `src/DigitalData.EmailProfiler.API/Workers/`
**EmailPollingWorker.cs** and **EmailSenderWorker.cs** (See agents.md for implementation examples)
#### 4.5. Update appsettings.json
**Location**: `src/DigitalData.EmailProfiler.API/appsettings.json`
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(local);Database=DD_ECM;Integrated Security=true;TrustServerCertificate=true"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
},
"Workers": {
"EmailPolling": {
"Enabled": true,
"IntervalSeconds": 60
},
"EmailSender": {
"Enabled": true,
"IntervalSeconds": 5
}
},
"Hosting": {
"Mode": "IIS"
}
}
```
---
### PHASE 5: Testing
#### 5.1. Add NuGet Packages
```bash
cd tests/DigitalData.EmailProfiler.Tests
dotnet add package FakeItEasy
dotnet add package Bogus
dotnet add package FluentAssertions
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Testcontainers.MsSql
```
#### 5.2. Create Unit Tests
**Location**: `tests/DigitalData.EmailProfiler.Tests/Unit/Domain/`
**MessageIdGeneratorTests.cs**:
```csharp
using Xunit;
using FluentAssertions;
using DigitalData.EmailProfiler.Domain.Services;
namespace DigitalData.EmailProfiler.Tests.Unit.Domain;
public class MessageIdGeneratorTests
{
[Fact]
public void Generate_ShouldCreateValidMessageId()
{
// Arrange
var generator = new MessageIdGenerator();
var original = "test-msg-123";
var sender = "sender@example.com";
var date = new DateTime(2026, 1, 1, 12, 0, 0);
var subject = "Test Subject";
// Act
var messageId = generator.Generate(original, sender, date, subject);
// Assert
messageId.Should().NotBeNull();
messageId.Hash.Should().NotBeNullOrEmpty();
messageId.Value.Should().Contain(original);
messageId.Value.Should().Contain(sender);
}
[Fact]
public void Generate_SameInput_ShouldProduceSameHash()
{
// Arrange
var generator = new MessageIdGenerator();
var original = "test-msg-123";
var sender = "sender@example.com";
var date = new DateTime(2026, 1, 1, 12, 0, 0);
var subject = "Test Subject";
// Act
var messageId1 = generator.Generate(original, sender, date, subject);
var messageId2 = generator.Generate(original, sender, date, subject);
// Assert
messageId1.Hash.Should().Be(messageId2.Hash);
}
}
```
#### 5.3. Create Integration Tests
Use Testcontainers for database integration tests.
---
### PHASE 6: Documentation
#### 6.1. Create README.md (in German)
**Location**: `README.md`
The README should include (in German):
- Application overview
- Architecture diagram
- API endpoints documentation
- Worker processes description
- Database tables documentation
- Configuration guide (appsettings.json)
- Deployment instructions (IIS and Windows Service)
- Troubleshooting guide
**Template structure**:
```markdown
# DigitalData EmailProfiler
## Übersicht
[Application overview in German]
## Architektur
[Architecture description]
## API Endpunkte
### Email Profile Management
- GET /api/emailprofiles - Alle Profile abrufen
- GET /api/emailprofiles/{id} - Profil nach ID abrufen
- POST /api/emailprofiles - Neues Profil erstellen
- PUT /api/emailprofiles/{id} - Profil aktualisieren
- DELETE /api/emailprofiles/{id} - Profil löschen
[... continue for all controllers]
## Background Workers
### EmailPollingWorker
Überwacht E-Mail-Konten und verarbeitet eingehende E-Mails.
**Konfiguration**:
```json
"Workers": {
"EmailPolling": {
"Enabled": true,
"IntervalSeconds": 60
}
}
```
[... continue for all workers]
## Datenbank Tabellen
### TBDD_EMAIL_ACCOUNT
[Table description]
[... continue for all tables]
## Konfiguration
[Detailed configuration guide]
## Deployment
### IIS Deployment
[Step-by-step guide]
### Windows Service Deployment
[Step-by-step guide]
```
---
## Build and Test Commands
```bash
# Build solution
dotnet build
# Run tests
dotnet test
# Run API
cd src/DigitalData.EmailProfiler.API
dotnet run
# Create migration
cd src/DigitalData.EmailProfiler.Infrastructure
dotnet ef migrations add InitialCreate --startup-project ../DigitalData.EmailProfiler.API
# Update database
dotnet ef database update --startup-project ../DigitalData.EmailProfiler.API
```
---
## Important Reminders for AI Agents
1. **NEVER modify database schema** - use `[Table]` and `[Column]` attributes
2. **NEVER commit to git** - wait for user instruction
3. **All code and comments in English** - except README.md (German)
4. **Use Serilog for logging** - structured logging
5. **Worker intervals configurable** - via appsettings.json
6. **Support IIS and Windows Service** - via configuration
7. **Check for database triggers** - add to DbContext if they exist
8. **RabbitMQ is future enhancement** - currently use InMemoryEmailQueue
---
## Next Steps for Continuation
1. Complete Application Layer (Commands, Queries, Validators)
2. Complete Infrastructure Layer (DbContext, Repositories, Services)
3. Complete API Layer (Controllers, Workers, Middleware)
4. Create comprehensive tests
5. Write README.md in German
6. Build and test the complete application
---
**Document Version**: 1.0
**Last Updated**: 2026-07-07
**Status**: Phase 1 Complete, Phase 2-6 Pending

266
STATUS.md Normal file
View File

@@ -0,0 +1,266 @@
# EmailProfiler - Current Implementation Status
**Last Updated**: 2026-07-07
---
## ✅ COMPLETED (Phase 1: Domain Layer - 100%)
### Entities (with proper [Table] and [Column] attributes)
-`EmailAccount.cs` - Maps to `TBDD_EMAIL_ACCOUNT`
-`EmailProfile.cs` - Maps to `TBEMLP_POLL_PROFILES`
-`EmailProcess.cs` - Maps to `TBEMLP_POLL_PROCESS`
-`ProcessStep.cs` - Maps to `TBEMLP_POLL_STEPS`
-`IndexingStep.cs` - Maps to `TBEMLP_POLL_INDEXING_STEPS`
-`EmailHistory.cs` - Maps to `TBEMLP_HISTORY`
-`EmailAttachment.cs` - Maps to `TBEMLP_HISTORY_ATTACHMENT`
-`EmailOutbox.cs` - Maps to `TBEMLP_EMAIL_OUT`
### Value Objects
-`MessageId.cs` - Uses SHA256 hash (legacy-compatible algorithm)
-`EmailAddress.cs` - Email validation and parsing
### Enums
-`ErrorCode.cs` - All error codes from legacy system
-`ProcessType.cs` - ProcessManager, AttachmentSniffer, ZugFeRDParser
-`AuthenticationType.cs` - UsernamePassword, OAuth2
-`EmailStatus.cs` - Email processing status
-`AttachmentStatus.cs` - Attachment validation status
### Common Classes
-`BaseEntity.cs` - Base class with audit fields
-`IAggregateRoot.cs` - DDD aggregate root marker
-`ValueObject.cs` - Base class for value objects
### Domain Services
-`MessageIdGenerator.cs` - Generates unique message IDs with legacy-compatible hash
### Domain Events
-`EmailProcessedEvent.cs` - MediatR event for email processing
### Exceptions
-`DomainException.cs` - Base domain exception
-`ValidationException.cs` - Validation errors
-`AttachmentProcessingException.cs` - Attachment-specific errors
### NuGet Packages
- ✅ Domain project has MediatR 12.2.0
---
## 🚧 IN PROGRESS (Phase 2: Application Layer - 5%)
### DTOs
-`CommonDtos.cs` - EmailProfileDto, EmailAccountDto, EmailHistoryDto, EmailAttachmentDto
### NuGet Packages
- ✅ Application project has:
- MediatR 14.2.0
- AutoMapper.Extensions.Microsoft.DependencyInjection 12.0.1
- FluentValidation.DependencyInjectionExtensions 12.1.1
---
## ❌ TODO (Phase 2: Application Layer - 95%)
### Repository Interfaces
-`IEmailProfileRepository.cs`
-`IEmailAccountRepository.cs`
-`IEmailHistoryRepository.cs`
-`IEmailProcessRepository.cs`
-`IEmailOutboxRepository.cs`
### Service Interfaces
-`IEmailService.cs`
-`IPdfProcessingService.cs`
-`IDmsService.cs`
-`IEncryptionService.cs`
-`IEmailQueue.cs`
### MediatR Commands
-`CreateEmailProfileCommand.cs`
-`UpdateEmailProfileCommand.cs`
-`DeleteEmailProfileCommand.cs`
-`ActivateProfileCommand.cs`
-`DeactivateProfileCommand.cs`
- ❌ Similar commands for EmailAccount, EmailProcess, etc.
### MediatR Queries
-`GetEmailProfilesQuery.cs`
-`GetEmailProfileByIdQuery.cs`
-`GetActiveProfilesQuery.cs`
-`GetProfilesDueForPollingQuery.cs`
- ❌ Similar queries for other entities
### Validators
-`CreateEmailProfileCommandValidator.cs`
-`UpdateEmailProfileCommandValidator.cs`
- ❌ Similar validators for all commands
### Mappings
-`MappingProfile.cs` - AutoMapper configuration
### DI Configuration
-`DependencyInjection.cs` - Application layer DI setup
---
## ❌ TODO (Phase 3: Infrastructure Layer - 0%)
### NuGet Packages
- ❌ Microsoft.EntityFrameworkCore.SqlServer
- ❌ Microsoft.EntityFrameworkCore.Tools
- ❌ MailKit
- ❌ MimeKit
- ❌ PdfSharp
- ❌ Microsoft.Identity.Client
- ❌ Microsoft.AspNetCore.DataProtection
### Persistence
-`EmailProfilerDbContext.cs`
- ❌ EF Core migrations
### Repositories
-`EmailProfileRepository.cs`
-`EmailAccountRepository.cs`
-`EmailHistoryRepository.cs`
-`EmailProcessRepository.cs`
-`EmailOutboxRepository.cs`
### External Services
-`MailKitEmailService.cs` - IMAP/SMTP with OAuth2
-`PdfSharpProcessingService.cs` - PDF validation and embedded file extraction
-`WindreamDmsService.cs` - windream DMS integration (COM Interop)
-`DataProtectionEncryptionService.cs` - Password encryption
-`InMemoryEmailQueue.cs` - Email queue (Channel-based)
### DI Configuration
-`DependencyInjection.cs` - Infrastructure layer DI setup
---
## ❌ TODO (Phase 4: API Layer - 0%)
### NuGet Packages
- ❌ Serilog.AspNetCore
- ❌ Serilog.Sinks.File
- ❌ Serilog.Sinks.MSSqlServer
- ❌ Scalar.AspNetCore
### Controllers
-`EmailProfilesController.cs`
-`EmailAccountsController.cs`
-`EmailHistoryController.cs`
-`DashboardController.cs`
### Background Workers
-`EmailPollingWorker.cs` - Monitors email accounts
-`EmailSenderWorker.cs` - Sends queued emails
### Middleware
-`ExceptionHandlingMiddleware.cs`
### Configuration
- ❌ Update `Program.cs` - Serilog, Scalar, DI, Windows Service support
- ❌ Update `appsettings.json` - Complete configuration
---
## ❌ TODO (Phase 5: Testing - 0%)
### NuGet Packages
- ❌ FakeItEasy
- ❌ Bogus
- ❌ FluentAssertions
- ❌ Microsoft.AspNetCore.Mvc.Testing
- ❌ Testcontainers.MsSql
### Unit Tests
- ❌ Domain entity tests
- ❌ Value object tests
- ❌ MessageIdGenerator tests
- ❌ Command handler tests
- ❌ Query handler tests
### Integration Tests
- ❌ Repository tests (with Testcontainers)
- ❌ API tests (with WebApplicationFactory)
---
## ❌ TODO (Phase 6: Documentation - 0%)
-`README.md` - Comprehensive documentation in German
- Application overview
- API endpoints
- Workers documentation
- Database tables
- Configuration guide
- Deployment guide (IIS + Windows Service)
---
## Build Status
**Solution builds successfully** (as of 2026-07-07)
```
Build succeeded.
0 Warning(s)
0 Error(s)
```
---
## Key Files for Reference
-`agents.md` - Important notes for future development
-`IMPLEMENTATION_GUIDE.md` - Step-by-step implementation guide
-`STATUS.md` - This file (current status)
-`MIGRATION_PLAN.md` - Full migration plan (not created yet)
---
## Next Agent Tasks
**Priority 1**: Complete Application Layer
1. Create all repository interfaces
2. Create all service interfaces
3. Create MediatR commands and handlers
4. Create MediatR queries and handlers
5. Create FluentValidation validators
6. Create AutoMapper profile
7. Create DependencyInjection.cs
**Priority 2**: Complete Infrastructure Layer
1. Add NuGet packages
2. Create EmailProfilerDbContext
3. Create all repositories
4. Create all external services
5. Create DependencyInjection.cs
6. Create initial EF Core migration
**Priority 3**: Complete API Layer
1. Add NuGet packages
2. Update Program.cs
3. Create all controllers
4. Create background workers
5. Update appsettings.json
**Priority 4**: Testing
1. Add test NuGet packages
2. Create unit tests
3. Create integration tests
**Priority 5**: Documentation
1. Create README.md (German)
---
**Total Progress**: ~15% complete
- Phase 1 (Domain): 100% ✅
- Phase 2 (Application): 5% 🚧
- Phase 3 (Infrastructure): 0% ❌
- Phase 4 (API): 0% ❌
- Phase 5 (Testing): 0% ❌
- Phase 6 (Documentation): 0% ❌

402
agents.md Normal file
View File

@@ -0,0 +1,402 @@
# EmailProfiler - Agent Notes and Future Enhancements
## Purpose
This document contains important notes, decisions, and future enhancement plans for the EmailProfiler application. This is intended for AI agents and developers who will continue development.
---
## Important Notes
### 1. Database Schema - DO NOT MODIFY
**CRITICAL**: The database schema must NEVER be modified. All Entity Framework entities must map to existing legacy tables using `[Table]` and `[Column]` attributes.
**Naming Convention**:
- Database: `SNAKE_CASE` with prefixes (TBEMLP_, TBDD_)
- C# Entities: `PascalCase` without prefixes
- Use `[Table("TBDD_FOO")]` and `[Column("COLUMN_NAME")]` attributes
**Example**:
```csharp
[Table("TBDD_EMAIL_ACCOUNT")]
public class EmailAccount
{
[Column("EMAIL_ACCOUNT_ID")]
public int Id { get; set; }
[Column("ACCOUNT_NAME")]
public string AccountName { get; set; }
}
```
### 2. Message ID Hash Algorithm
The `MessageIdGenerator` in `Domain.Services` must use **exactly the same algorithm** as the legacy system to ensure duplicate detection works correctly.
**Algorithm**: SHA256 hash of `{originalMessageId}|{sender}|{date:yyyyMMddHHmmss}|{subject}`
### 3. No Commits Without Permission
**NEVER** commit changes to git automatically. Always wait for explicit user instruction to commit.
---
## Future Enhancements
### HIGH PRIORITY: RabbitMQ Queue Implementation
**Current State**:
- Email queue is implemented using in-memory `Channel<T>` in `InMemoryEmailQueue.cs`
- Location: `src/DigitalData.EmailProfiler.Infrastructure/Queue/InMemoryEmailQueue.cs`
**Future Enhancement**:
Replace the in-memory queue with **RabbitMQ** for production resilience and scalability.
**Implementation Steps**:
1. **Add NuGet Package**:
```bash
dotnet add package RabbitMQ.Client
```
2. **Create RabbitMqEmailQueue.cs**:
```csharp
// src/DigitalData.EmailProfiler.Infrastructure/Queue/RabbitMqEmailQueue.cs
public class RabbitMqEmailQueue : IEmailQueue
{
private readonly IConnection _connection;
private readonly IModel _channel;
private const string QueueName = "email-outbox";
public RabbitMqEmailQueue(IOptions<RabbitMqConfiguration> config)
{
var factory = new ConnectionFactory
{
HostName = config.Value.HostName,
Port = config.Value.Port,
UserName = config.Value.UserName,
Password = config.Value.Password
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(
queue: QueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
}
public async Task EnqueueAsync(OutgoingEmail email, CancellationToken cancellationToken)
{
var json = JsonSerializer.Serialize(email);
var body = Encoding.UTF8.GetBytes(json);
var properties = _channel.CreateBasicProperties();
properties.Persistent = true;
_channel.BasicPublish(
exchange: "",
routingKey: QueueName,
basicProperties: properties,
body: body);
await Task.CompletedTask;
}
public async Task<OutgoingEmail?> DequeueAsync(CancellationToken cancellationToken)
{
var result = _channel.BasicGet(QueueName, autoAck: false);
if (result == null)
return null;
var json = Encoding.UTF8.GetString(result.Body.ToArray());
var email = JsonSerializer.Deserialize<OutgoingEmail>(json);
_channel.BasicAck(result.DeliveryTag, false);
return await Task.FromResult(email);
}
}
```
3. **Configuration** (appsettings.json):
```json
{
"RabbitMq": {
"HostName": "localhost",
"Port": 5672,
"UserName": "guest",
"Password": "guest"
}
}
```
4. **Dependency Injection** (Program.cs):
```csharp
// Replace InMemoryEmailQueue with RabbitMqEmailQueue
// builder.Services.AddSingleton<IEmailQueue, InMemoryEmailQueue>();
builder.Services.AddSingleton<IEmailQueue, RabbitMqEmailQueue>();
```
**Benefits**:
- Message persistence (survives application restarts)
- Scalability (multiple worker instances can consume from queue)
- Reliability (automatic retries, dead letter queues)
- Monitoring (RabbitMQ management UI)
**Migration Path**:
1. Deploy RabbitMQ server (Docker recommended)
2. Test RabbitMqEmailQueue in staging environment
3. Switch DI registration from InMemoryEmailQueue to RabbitMqEmailQueue
4. Monitor queue depth and worker performance
---
## Pending Implementation Tasks
### Phase 2: Application Layer (IN PROGRESS)
**Status**: Partially complete - DTOs created, Commands/Queries needed
**TODO**:
- [ ] Create MediatR Commands (CreateEmailProfileCommand, ProcessEmailCommand, etc.)
- [ ] Create MediatR Queries (GetEmailProfilesQuery, GetEmailHistoryQuery, etc.)
- [ ] Create Command/Query Handlers
- [ ] Create FluentValidation Validators
- [ ] Create AutoMapper Profiles
- [ ] Create Application Interfaces (IEmailService, IPdfProcessingService, IDmsService, etc.)
**Example Command**:
```csharp
// src/DigitalData.EmailProfiler.Application/EmailProfiles/Commands/CreateEmailProfileCommand.cs
public record CreateEmailProfileCommand(string ProfileName, int EmailAccountId) : IRequest<int>;
public class CreateEmailProfileCommandHandler : IRequestHandler<CreateEmailProfileCommand, int>
{
private readonly IEmailProfileRepository _repository;
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
{
var profile = new EmailProfile
{
ProfileName = request.ProfileName,
EmailAccountId = request.EmailAccountId,
IsActive = true
};
await _repository.AddAsync(profile, cancellationToken);
return profile.Id;
}
}
```
### Phase 3: Infrastructure Layer
**Status**: Not started
**TODO**:
- [ ] Create EmailProfilerDbContext with DbSet<T> for all entities
- [ ] Create Entity Configurations (Fluent API) for all entities
- [ ] Create Repositories implementing Application interfaces
- [ ] Create MailKitEmailService (IMAP/SMTP with OAuth2)
- [ ] Create PdfSharpProcessingService
- [ ] Create WindreamDmsService (COM Interop)
- [ ] Create EncryptionService (Data Protection API)
- [ ] Create initial EF Core migration
**DbContext Example**:
```csharp
public class EmailProfilerDbContext : DbContext
{
public DbSet<EmailAccount> EmailAccounts { get; set; }
public DbSet<EmailProfile> EmailProfiles { get; set; }
// ... other DbSets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
// Important: Check for triggers
modelBuilder.Entity<EmailHistory>().ToTable(tb => tb.HasTrigger("TR_TBEMLP_HISTORY_AUDIT"));
}
}
```
### Phase 4: API Layer
**Status**: Minimal structure exists
**TODO**:
- [ ] Create Controllers (ProfilesController, EmailAccountsController, HistoryController)
- [ ] Create Background Workers (EmailPollingWorker, EmailSenderWorker)
- [ ] Configure Serilog
- [ ] Configure Scalar (OpenAPI documentation)
- [ ] Add Exception Handling Middleware
- [ ] Configure DI for all layers
- [ ] Support both IIS and Windows Service hosting
**Worker Configuration** (appsettings.json):
```json
{
"Workers": {
"EmailPolling": {
"Enabled": true,
"IntervalSeconds": 60
},
"EmailSender": {
"Enabled": true,
"IntervalSeconds": 5
}
},
"Hosting": {
"Mode": "IIS" // or "WindowsService"
}
}
```
### Phase 5: Testing
**Status**: Not started
**TODO**:
- [ ] Unit tests for Domain entities
- [ ] Unit tests for Application handlers (using FakeItEasy)
- [ ] Integration tests for Repositories (using Testcontainers)
- [ ] API tests (using WebApplicationFactory)
- [ ] Generate fake test data (using Bogus)
**Test Example**:
```csharp
public class MessageIdGeneratorTests
{
[Fact]
public void Generate_ShouldProduceSameHashAsLegacy()
{
// Arrange
var generator = new MessageIdGenerator();
var original = "msg-123";
var sender = "test@example.com";
var date = new DateTime(2026, 1, 1, 12, 0, 0);
var subject = "Test Subject";
// Act
var messageId = generator.Generate(original, sender, date, subject);
// Assert
messageId.Hash.Should().NotBeNullOrEmpty();
// TODO: Verify against known legacy hash
}
}
```
---
## Architecture Decisions
### Clean Architecture Layers
1. **Domain**: Core business logic, no dependencies
2. **Application**: Use cases, depends on Domain
3. **Infrastructure**: External concerns, depends on Domain + Application
4. **API**: Entry point, depends on all
### CQRS Pattern with MediatR
- **Commands**: Modify state (Create, Update, Delete)
- **Queries**: Read data (Get, List)
- Separate models for read and write operations
### Repository Pattern
- Interface in Application layer
- Implementation in Infrastructure layer
- One repository per Aggregate Root
---
## Known Issues and Limitations
### 1. PdfSharp Embedded File Extraction
PdfSharp has limited support for embedded file extraction from PDFs. If advanced PDF processing is needed, consider:
- **iText7** (AGPL or commercial license)
- **Aspose.PDF** (commercial license)
- Custom PDF parsing using PDF specification
### 2. windream COM Interop
The windream DMS integration uses COM Interop which is Windows-only. The application cannot be fully cross-platform unless windream provides a REST API alternative.
### 3. OAuth2 Token Refresh
Current implementation acquires new tokens on each request. Consider implementing token caching:
- Use `Microsoft.Identity.Web` for automatic token management
- Cache tokens in memory or distributed cache (Redis)
---
## Development Guidelines
### 1. Code Style
- All code and comments: **English**
- README.md and user documentation: **German**
- Follow C# naming conventions (PascalCase, camelCase)
- Use nullable reference types (`#nullable enable`)
### 2. Logging
Use Serilog with structured logging:
```csharp
_logger.LogInformation("Processing email {MessageId} from profile {ProfileId}", messageId, profileId);
```
### 3. Configuration
- Development: `appsettings.Development.json` + User Secrets
- Production: `appsettings.json` + Environment Variables + Azure Key Vault
### 4. Error Handling
- Domain: Throw `DomainException` for business rule violations
- Application: Use `FluentValidation` for input validation
- API: Use exception handling middleware to return proper HTTP status codes
---
## Deployment Scenarios
### IIS Hosting (Default)
```json
{
"Hosting": {
"Mode": "IIS"
}
}
```
### Windows Service Hosting
```json
{
"Hosting": {
"Mode": "WindowsService"
}
}
```
In `Program.cs`:
```csharp
var builder = WebApplication.CreateBuilder(args);
if (builder.Configuration["Hosting:Mode"] == "WindowsService")
{
builder.Host.UseWindowsService();
}
```
Install as Windows Service:
```bash
sc create EmailProfiler binPath="C:\Path\To\DigitalData.EmailProfiler.API.exe"
```
---
## Contact and Support
For questions about this implementation, consult:
- Legacy system analysis: `legacy/PROJECT_ANALYSIS.md`
- Migration plan: `MIGRATION_PLAN.md` (if created)
- This document: `agents.md`
---
**Last Updated**: 2026-07-07
**Version**: 1.0
**Status**: Phase 1 Complete (Domain Layer), Phase 2-8 Pending

1
legacy Submodule

Submodule legacy added at 8a0011394b

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-DigitalData.EmailProfiler.Service-e7ef6a9a-436f-48a1-b4f6-ec9381c8cb47</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DigitalData.EmailProfiler.Application\DigitalData.EmailProfiler.Application.csproj" />
<ProjectReference Include="..\DigitalData.EmailProfiler.Domain\DigitalData.EmailProfiler.Domain.csproj" />
<ProjectReference Include="..\DigitalData.EmailProfiler.Infrastructure\DigitalData.EmailProfiler.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using DigitalData.EmailProfiler.API;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:60519",
"sslPort": 44302
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5207",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7261;http://localhost:5207",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,16 @@
namespace DigitalData.EmailProfiler.API;
public class Worker(ILogger<Worker> Logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (Logger.IsEnabled(LogLevel.Information))
{
Logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,54 @@
namespace DigitalData.EmailProfiler.Application.Common.Dtos;
public class EmailProfileDto
{
public int Id { get; set; }
public string ProfileName { get; set; } = string.Empty;
public int EmailAccountId { get; set; }
public string? EmailAccountName { get; set; }
public int? ProcessId { get; set; }
public string? ProcessName { get; set; }
public int PollIntervalMinutes { get; set; }
public DateTime? LastPollTime { get; set; }
public bool IsActive { get; set; }
public string? Comment { get; set; }
}
public class EmailAccountDto
{
public int Id { get; set; }
public string AccountName { get; set; } = string.Empty;
public string ImapServer { get; set; } = string.Empty;
public int ImapPort { get; set; }
public string SmtpServer { get; set; } = string.Empty;
public int SmtpPort { get; set; }
public string Username { get; set; } = string.Empty;
public bool UseOAuth2 { get; set; }
public bool IsActive { get; set; }
}
public class EmailHistoryDto
{
public int Id { get; set; }
public int? ProfileId { get; set; }
public string? ProfileName { get; set; }
public string? SenderAddress { get; set; }
public string? Subject { get; set; }
public DateTime? EmailDate { get; set; }
public DateTime? ProcessedDate { get; set; }
public string? Status { get; set; }
public int? ErrorCodeValue { get; set; }
public string? ErrorMessage { get; set; }
public List<EmailAttachmentDto> Attachments { get; set; } = new();
}
public class EmailAttachmentDto
{
public int Id { get; set; }
public string OriginalFileName { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty;
public long FileSize { get; set; }
public bool IsEmbeddedFile { get; set; }
public string? Status { get; set; }
public string? ValidationErrorMessage { get; set; }
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DigitalData.EmailProfiler.Domain\DigitalData.EmailProfiler.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace DigitalData.EmailProfiler.Domain.Common;
public abstract class BaseEntity
{
public DateTime? CreatedDate { get; set; }
public string? CreatedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
public string? ModifiedBy { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace DigitalData.EmailProfiler.Domain.Common;
/// <summary>
/// Marker interface for aggregate roots in DDD
/// </summary>
public interface IAggregateRoot
{
}

View File

@@ -0,0 +1,43 @@
namespace DigitalData.EmailProfiler.Domain.Common;
/// <summary>
/// Base class for value objects that implement equality by value
/// </summary>
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
public static bool operator !=(ValueObject? left, ValueObject? right)
{
return !(left == right);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,107 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBDD_EMAIL_ACCOUNT")]
public class EmailAccount : BaseEntity, IAggregateRoot
{
[Key]
[Column("EMAIL_ACCOUNT_ID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("ACCOUNT_NAME", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string AccountName { get; set; } = string.Empty;
// IMAP Configuration
[Column("IMAP_SERVER", TypeName = "varchar(200)")]
[Required]
[MaxLength(200)]
public string ImapServer { get; set; } = string.Empty;
[Column("IMAP_PORT")]
[Required]
public int ImapPort { get; set; }
[Column("IMAP_USE_SSL")]
public bool ImapUseSsl { get; set; } = true;
// SMTP Configuration
[Column("SMTP_SERVER", TypeName = "varchar(200)")]
[Required]
[MaxLength(200)]
public string SmtpServer { get; set; } = string.Empty;
[Column("SMTP_PORT")]
[Required]
public int SmtpPort { get; set; }
[Column("SMTP_USE_SSL")]
public bool SmtpUseSsl { get; set; } = true;
// Authentication
[Column("USERNAME", TypeName = "varchar(200)")]
[Required]
[MaxLength(200)]
public string Username { get; set; } = string.Empty;
[Column("PASSWORD", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? EncryptedPassword { get; set; }
[Column("USE_OAUTH2")]
public bool UseOAuth2 { get; set; }
// OAuth2 (nullable)
[Column("CLIENT_ID", TypeName = "varchar(200)")]
[MaxLength(200)]
public string? ClientId { get; set; }
[Column("TENANT_ID", TypeName = "varchar(200)")]
[MaxLength(200)]
public string? TenantId { get; set; }
[Column("CLIENT_SECRET", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? EncryptedClientSecret { get; set; }
// Status
[Column("ACTIVE")]
public bool IsActive { get; set; } = true;
[Column("SEQUENCE")]
public int? Sequence { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit fields
[Column("ADDED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
[Column("CHANGED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
// Navigation properties
public virtual ICollection<EmailProfile> Profiles { get; set; } = new List<EmailProfile>();
// Domain methods
public AuthenticationType GetAuthenticationType() => UseOAuth2 ? AuthenticationType.OAuth2 : AuthenticationType.UsernamePassword;
public void Activate() => IsActive = true;
public void Deactivate() => IsActive = false;
}

View File

@@ -0,0 +1,93 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_HISTORY_ATTACHMENT")]
public class EmailAttachment : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("EMAIL_HISTORY_ID")]
[Required]
public int EmailHistoryId { get; set; }
// File information
[Column("ORIGINAL_FILE_NAME", TypeName = "varchar(500)")]
[Required]
[MaxLength(500)]
public string OriginalFileName { get; set; } = string.Empty;
[Column("SAVED_FILE_NAME", TypeName = "varchar(500)")]
[Required]
[MaxLength(500)]
public string SavedFileName { get; set; } = string.Empty;
[Column("FILE_PATH", TypeName = "varchar(1000)")]
[Required]
[MaxLength(1000)]
public string FilePath { get; set; } = string.Empty;
[Column("FILE_SIZE")]
public long FileSize { get; set; }
[Column("EXTENSION", TypeName = "varchar(20)")]
[MaxLength(20)]
public string Extension { get; set; } = string.Empty;
// Attachment hierarchy
[Column("IS_EMBEDDED_FILE")]
public bool IsEmbeddedFile { get; set; }
[Column("PARENT_ATTACHMENT_ID")]
public int? ParentAttachmentId { get; set; }
[Column("ATTACHMENT_POSITION")]
public int AttachmentPosition { get; set; }
// Validation
[Column("STATUS", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? Status { get; set; }
[Column("VALIDATION_ERROR_CODE")]
public int? ValidationErrorCode { get; set; }
[Column("VALIDATION_ERROR_MESSAGE", TypeName = "nvarchar(max)")]
public string? ValidationErrorMessage { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit field
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
// Navigation properties
[ForeignKey("EmailHistoryId")]
public virtual EmailHistory? EmailHistory { get; set; }
[ForeignKey("ParentAttachmentId")]
public virtual EmailAttachment? ParentAttachment { get; set; }
public virtual ICollection<EmailAttachment> EmbeddedFiles { get; set; } = new List<EmailAttachment>();
// Domain methods
public void MarkAsValid()
{
Status = AttachmentStatus.Valid.ToString();
}
public void MarkAsCorrupt(ErrorCode errorCode, string message)
{
Status = AttachmentStatus.Corrupt.ToString();
ValidationErrorCode = (int)errorCode;
ValidationErrorMessage = message;
}
}

View File

@@ -0,0 +1,112 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_HISTORY")]
public class EmailHistory : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("PROFILE_ID")]
public int? ProfileId { get; set; }
[Column("WORK_PROCESS", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? WorkProcess { get; set; }
// Email identification
[Column("EMAIL_MSGID")]
[Required]
public int EmailMessageId { get; set; }
[Column("EMAIL_MSGID_HASH", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? MessageIdHash { get; set; }
[Column("EMAIL_MSGID_ORIGINAL", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? OriginalMessageId { get; set; }
[Column("IMAP_UID")]
public int? ImapUid { get; set; }
// Email metadata
[Column("EMAIL_SENDER", TypeName = "varchar(200)")]
[MaxLength(200)]
public string? SenderAddress { get; set; }
[Column("EMAIL_SUBJECT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Subject { get; set; }
[Column("EMAIL_DATE")]
public DateTime? EmailDate { get; set; }
[Column("EMAIL_BODY", TypeName = "nvarchar(max)")]
public string? EmailBodyHtml { get; set; }
[Column("EMAIL_BODY_TEXT", TypeName = "nvarchar(max)")]
public string? EmailBodyText { get; set; }
[Column("EMAIL_SUBSTRING1", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? EmailSubstring1 { get; set; }
[Column("EMAIL_SUBSTRING2", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? EmailSubstring2 { get; set; }
// Processing
[Column("PROCESSED_DATE")]
public DateTime? ProcessedDate { get; set; }
[Column("STATUS", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? Status { get; set; }
[Column("ERROR_CODE")]
public int? ErrorCodeValue { get; set; }
[Column("ERROR_MESSAGE", TypeName = "nvarchar(max)")]
public string? ErrorMessage { get; set; }
// windream
[Column("WINDREAM_DOCUMENT_ID", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? WindreamDocumentId { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit field
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
// Navigation properties
[ForeignKey("ProfileId")]
public virtual EmailProfile? Profile { get; set; }
public virtual ICollection<EmailAttachment> Attachments { get; set; } = new List<EmailAttachment>();
// Domain methods
public void MarkAsProcessed()
{
Status = EmailStatus.Processed.ToString();
ProcessedDate = DateTime.UtcNow;
}
public void MarkAsFailed(ErrorCode errorCode, string message)
{
Status = EmailStatus.Failed.ToString();
ErrorCodeValue = (int)errorCode;
ErrorMessage = message;
ProcessedDate = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,60 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_EMAIL_OUT")]
public class EmailOutbox : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("EMAIL_ACCOUNT_ID")]
[Required]
public int EmailAccountId { get; set; }
[Column("RECIPIENT", TypeName = "varchar(200)")]
[Required]
[MaxLength(200)]
public string Recipient { get; set; } = string.Empty;
[Column("SUBJECT", TypeName = "nvarchar(500)")]
[Required]
[MaxLength(500)]
public string Subject { get; set; } = string.Empty;
[Column("BODY", TypeName = "nvarchar(max)")]
[Required]
public string Body { get; set; } = string.Empty;
[Column("IS_HTML")]
public bool IsHtml { get; set; } = true;
[Column("SENT")]
public bool Sent { get; set; }
[Column("SENT_DATE")]
public DateTime? SentDate { get; set; }
[Column("RETRY_COUNT")]
public int RetryCount { get; set; }
[Column("REFERENCE_STRING", TypeName = "varchar(200)")]
[MaxLength(200)]
public string? ReferenceId { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit field
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
// Navigation properties
[ForeignKey("EmailAccountId")]
public virtual EmailAccount? EmailAccount { get; set; }
}

View File

@@ -0,0 +1,114 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_POLL_PROCESS")]
public class EmailProcess : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("PROCESS_NAME", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string ProcessName { get; set; } = string.Empty;
[Column("STEP_NAME", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string StepName { get; set; } = string.Empty;
[Column("PROFILE_ID")]
public int? ProfileId { get; set; }
// Configuration
[Column("COPY_2_HDD")]
public bool CopyToHdd { get; set; }
[Column("DELETE_MAIL")]
public bool DeleteEmailAfterProcessing { get; set; }
// Paths
[Column("PATH_EMAIL_TEMP", TypeName = "varchar(500)")]
[Required]
[MaxLength(500)]
public string TempPath { get; set; } = string.Empty;
[Column("PATH_EMAIL_ERRORS", TypeName = "varchar(500)")]
[Required]
[MaxLength(500)]
public string ErrorPath { get; set; } = string.Empty;
[Column("PATH_ORIGINAL", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? ArchivePath { get; set; }
// windream
[Column("WM_IMPORT")]
public bool EnableWindreamImport { get; set; }
[Column("WM_OBJEKTTYPE", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? WindreamObjectType { get; set; }
[Column("WM_VECTOR_LOG", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? WindreamVectorLog { get; set; }
[Column("WM_PATH", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? WindreamPath { get; set; }
[Column("WM_FILE_NAME", TypeName = "varchar(200)")]
[MaxLength(200)]
public string? WindreamFileName { get; set; }
[Column("WM_REFERENCE_INDEX", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? WindreamReferenceIndex { get; set; }
[Column("WM_IDX_BODY_TEXT", TypeName = "varchar(100)")]
[MaxLength(100)]
public string? WindreamIndexBodyText { get; set; }
[Column("WM_IDX_BODY_SUBSTR_LENGTH")]
public int WindreamIndexBodySubstrLength { get; set; }
[Column("ALLOW_XML_RECEIPTS")]
public bool? AllowXmlReceipts { get; set; }
// Status
[Column("ACTIVE")]
public bool IsActive { get; set; } = true;
[Column("SEQUENCE")]
public int? Sequence { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit fields
[Column("ADDED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
[Column("CHANGED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
// Navigation properties
public virtual ICollection<ProcessStep> Steps { get; set; } = new List<ProcessStep>();
public virtual ICollection<EmailProfile> Profiles { get; set; } = new List<EmailProfile>();
}

View File

@@ -0,0 +1,88 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_POLL_PROFILES")]
public class EmailProfile : BaseEntity, IAggregateRoot
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("PROFILE_NAME", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string ProfileName { get; set; } = string.Empty;
[Column("POLL_TYPE", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string PollType { get; set; } = "IMAP";
[Column("EMAIL_CONF_ID")]
[Required]
public int EmailAccountId { get; set; }
[Column("PROCESS_ID")]
public int? ProcessId { get; set; }
// Polling Configuration
[Column("POLL_INTERVAL")]
public int PollIntervalMinutes { get; set; } = 5;
[Column("LAST_TICK")]
public DateTime? LastPollTime { get; set; }
// Validation
[Column("VALIDATION_SQL", TypeName = "nvarchar(1024)")]
[MaxLength(1024)]
public string? ValidationSql { get; set; }
// Status
[Column("ACTIVE")]
public bool IsActive { get; set; }
[Column("SEQUENCE")]
public int? Sequence { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit fields
[Column("ADDED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
[Column("CHANGED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
// Navigation properties
[ForeignKey("EmailAccountId")]
public virtual EmailAccount? EmailAccount { get; set; }
[ForeignKey("ProcessId")]
public virtual EmailProcess? EmailProcess { get; set; }
public virtual ICollection<EmailHistory> EmailHistories { get; set; } = new List<EmailHistory>();
// Domain methods
public void UpdateLastPollTime() => LastPollTime = DateTime.UtcNow;
public bool ShouldPoll()
{
if (!IsActive) return false;
if (!LastPollTime.HasValue) return true;
return DateTime.UtcNow >= LastPollTime.Value.AddMinutes(PollIntervalMinutes);
}
}

View File

@@ -0,0 +1,56 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_POLL_INDEXING_STEPS")]
public class IndexingStep : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("STEP_ID")]
[Required]
public int StepId { get; set; }
[Column("INDEXNAME", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string IndexName { get; set; } = string.Empty;
[Column("INDEXVALUE", TypeName = "varchar(100)")]
[Required]
[MaxLength(100)]
public string IndexValue { get; set; } = string.Empty;
[Column("ACTIVE")]
public bool IsActive { get; set; } = true;
[Column("USE_FOR_DIRECT_ANSWER")]
public bool UseForDirectAnswer { get; set; }
[Column("SEQUENCE")]
public int? Sequence { get; set; }
// Audit fields
[Column("ADDED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
[Column("CHANGED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
// Navigation properties
[ForeignKey("StepId")]
public virtual ProcessStep? ProcessStep { get; set; }
}

View File

@@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DigitalData.EmailProfiler.Domain.Common;
namespace DigitalData.EmailProfiler.Domain.Entities;
[Table("TBEMLP_POLL_STEPS")]
public class ProcessStep : BaseEntity
{
[Key]
[Column("GUID", Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("PROCESS_ID")]
[Required]
public int ProcessId { get; set; }
[Column("STEP_NAME", TypeName = "varchar(50)")]
[Required]
[MaxLength(50)]
public string StepName { get; set; } = string.Empty;
[Column("KEYWORDS_BODY", TypeName = "varchar(1000)")]
[MaxLength(1000)]
public string? KeywordsBody { get; set; }
[Column("ACTIVE")]
public bool IsActive { get; set; } = true;
[Column("SEQUENCE")]
public int? Sequence { get; set; }
[Column("COMMENT", TypeName = "varchar(500)")]
[MaxLength(500)]
public string? Comment { get; set; }
// Audit fields
[Column("ADDED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? AddedWho { get; set; }
[Column("ADDED_WHEN")]
public DateTime? AddedWhen { get; set; }
[Column("CHANGED_WHO", TypeName = "varchar(50)")]
[MaxLength(50)]
public string? ChangedWho { get; set; }
[Column("CHANGED_WHEN")]
public DateTime? ChangedWhen { get; set; }
// Navigation properties
[ForeignKey("ProcessId")]
public virtual EmailProcess? EmailProcess { get; set; }
public virtual ICollection<IndexingStep> IndexingSteps { get; set; } = new List<IndexingStep>();
}

View File

@@ -0,0 +1,9 @@
namespace DigitalData.EmailProfiler.Domain.Enums;
public enum AttachmentStatus
{
Pending = 1,
Valid = 2,
Corrupt = 3,
Skipped = 4
}

View File

@@ -0,0 +1,7 @@
namespace DigitalData.EmailProfiler.Domain.Enums;
public enum AuthenticationType
{
UsernamePassword = 1,
OAuth2 = 2
}

View File

@@ -0,0 +1,11 @@
namespace DigitalData.EmailProfiler.Domain.Enums;
public enum EmailStatus
{
Pending = 1,
Processing = 2,
Processed = 3,
Failed = 4,
PartiallyProcessed = 5,
Rejected = 6
}

View File

@@ -0,0 +1,16 @@
namespace DigitalData.EmailProfiler.Domain.Enums;
public enum ErrorCode
{
None = 0,
NoAttachments = 10001,
SenderValidationFailed = 10002,
EmbeddedFileAttachmentCorrupt = 10003,
NormalFileAttachmentCorrupt = 10004,
PdfStructureInvalid = 10005,
ImapConnectionFailed = 10006,
WindreamImportFailed = 10007,
DiskSpaceInsufficient = 10008,
DuplicateMessageId = 10009,
AttachmentExtractionFailed = 10010
}

View File

@@ -0,0 +1,8 @@
namespace DigitalData.EmailProfiler.Domain.Enums;
public enum ProcessType
{
ProcessManager = 1, // Easy Approval workflow
AttachmentSniffer = 2, // General attachment extraction
ZugFeRDParser = 3 // Electronic invoice processing
}

View File

@@ -0,0 +1,22 @@
using MediatR;
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Events;
public class EmailProcessedEvent : INotification
{
public int EmailHistoryId { get; }
public int ProfileId { get; }
public string MessageId { get; }
public EmailStatus Status { get; }
public DateTime ProcessedDate { get; }
public EmailProcessedEvent(int emailHistoryId, int profileId, string messageId, EmailStatus status)
{
EmailHistoryId = emailHistoryId;
ProfileId = profileId;
MessageId = messageId;
Status = status;
ProcessedDate = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,19 @@
using DigitalData.EmailProfiler.Domain.Enums;
namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class AttachmentProcessingException : DomainException
{
public ErrorCode ErrorCode { get; }
public AttachmentProcessingException(ErrorCode errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
public AttachmentProcessingException(ErrorCode errorCode, string message, Exception innerException)
: base(message, innerException)
{
ErrorCode = errorCode;
}
}

View File

@@ -0,0 +1,12 @@
namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class DomainException : Exception
{
public DomainException(string message) : base(message)
{
}
public DomainException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -0,0 +1,12 @@
namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class ValidationException : DomainException
{
public ValidationException(string message) : base(message)
{
}
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -0,0 +1,28 @@
using DigitalData.EmailProfiler.Domain.ValueObjects;
namespace DigitalData.EmailProfiler.Domain.Services;
public interface IMessageIdGenerator
{
MessageId Generate(string originalMessageId, string sender, DateTime date, string subject);
List<string> GenerateFallbackHashes(MessageId messageId);
}
public class MessageIdGenerator : IMessageIdGenerator
{
public MessageId Generate(string originalMessageId, string sender, DateTime date, string subject)
{
return MessageId.Create(originalMessageId, sender, date, subject);
}
public List<string> GenerateFallbackHashes(MessageId messageId)
{
// Legacy behavior: 10 variations for duplicate detection
var hashes = new List<string> { messageId.Hash };
// Add variations (this is simplified - legacy had 10 variations)
// TODO: Implement exact legacy fallback algorithm if needed
return hashes;
}
}

View File

@@ -0,0 +1,45 @@
using System.ComponentModel.DataAnnotations;
using DigitalData.EmailProfiler.Domain.Common;
using DigitalData.EmailProfiler.Domain.Exceptions;
namespace DigitalData.EmailProfiler.Domain.ValueObjects;
/// <summary>
/// Value object representing an email address with validation
/// </summary>
public class EmailAddress : ValueObject
{
public string Value { get; private set; }
public string Domain { get; private set; }
public string LocalPart { get; private set; }
private EmailAddress(string value)
{
Value = value;
var parts = value.Split('@');
LocalPart = parts[0];
Domain = parts[1];
}
public static EmailAddress Create(string email)
{
if (!IsValid(email))
throw new DomainException($"Invalid email address: {email}");
return new EmailAddress(email.ToLowerInvariant());
}
private static bool IsValid(string email)
{
return !string.IsNullOrWhiteSpace(email) &&
email.Contains('@') &&
new EmailAddressAttribute().IsValid(email);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public override string ToString() => Value;
}

View File

@@ -0,0 +1,41 @@
using System.Security.Cryptography;
using System.Text;
using DigitalData.EmailProfiler.Domain.Common;
namespace DigitalData.EmailProfiler.Domain.ValueObjects;
/// <summary>
/// Value object representing a unique message identifier with hash generation
/// Uses the same algorithm as legacy system for compatibility
/// </summary>
public class MessageId : ValueObject
{
public string Value { get; private set; }
public string Hash { get; private set; }
private MessageId(string value)
{
Value = value;
Hash = GenerateHash(value);
}
public static MessageId Create(string originalMessageId, string sender, DateTime date, string subject)
{
var combined = $"{originalMessageId}|{sender}|{date:yyyyMMddHHmmss}|{subject}";
return new MessageId(combined);
}
private static string GenerateHash(string input)
{
// Same algorithm as legacy: SHA256 hash
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DigitalData.EmailProfiler.Domain\DigitalData.EmailProfiler.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DigitalData.EmailProfiler.API\DigitalData.EmailProfiler.API.csproj" />
<ProjectReference Include="..\..\src\DigitalData.EmailProfiler.Application\DigitalData.EmailProfiler.Application.csproj" />
<ProjectReference Include="..\..\src\DigitalData.EmailProfiler.Domain\DigitalData.EmailProfiler.Domain.csproj" />
<ProjectReference Include="..\..\src\DigitalData.EmailProfiler.Infrastructure\DigitalData.EmailProfiler.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>