Imports System.Collections.Generic Imports System.Data Imports System.IO Imports System.Linq Imports System.Security.Cryptography Imports DigitalData.Modules.Base Imports DigitalData.Modules.Database Imports DigitalData.Modules.Interfaces Imports DigitalData.Modules.Interfaces.Exceptions Imports DigitalData.Modules.Jobs.Exceptions Imports DigitalData.Modules.Logging Imports System.Data.SqlClient Public Class ImportZUGFeRDFiles Implements IJob Public Const ZUGFERD_IN = "ZUGFeRD in" Public Const ZUGFERD_ERROR = "ZUGFeRD Error" Public Const ZUGFERD_SUCCESS = "ZUGFeRD Success" Public Const ZUGFERD_EML = "ZUGFeRD Eml" Public Const ZUGFERD_REJECTED_EML = "ZUGFeRD Eml Rejected" Public Const ZUGFERD_ATTACHMENTS = "ZUGFeRD Attachments" Public Const ZUGFERD_NO_ZUGFERD = "Non-ZUGFeRD Files" Public HISTORY_ID As Integer Private Const DIRECTORY_DONT_MOVE = "DIRECTORY_DONT_MOVE" ' List of allowed extensions for PDF/A Attachments ' This list should not contain xml so the zugferd xml file will be filtered out Private ReadOnly AllowedExtensions As New List(Of String) From {"docx", "doc", "pdf", "xls", "xlsx", "ppt", "pptx", "txt"} Private ReadOnly _logger As Logger Private ReadOnly _logConfig As LogConfig Private ReadOnly _filesystem As Filesystem.File Private ReadOnly _mssql As MSSQLServer Private ReadOnly _email As ZUGFeRD.EmailFunctions Private ReadOnly _file As ZUGFeRD.FileFunctions Private ReadOnly _history As ZUGFeRD.HistoryFunctions Private ReadOnly _gdpictureLicenseKey As String Private _zugferd As ZUGFeRDInterface Private _EmailOutAccountId As Integer Private Class DatabaseConnections Public Property SQLServerConnection As SqlConnection Public Property SQLServerTransaction As SqlTransaction End Class Public Sub New(LogConfig As LogConfig, Optional MSSQL As MSSQLServer = Nothing) _logConfig = LogConfig _logger = LogConfig.GetLogger() _filesystem = New Filesystem.File(_logConfig) _mssql = MSSQL _email = New ZUGFeRD.EmailFunctions(LogConfig, _mssql) _file = New ZUGFeRD.FileFunctions(LogConfig, _mssql) _history = New ZUGFeRD.HistoryFunctions(LogConfig, _mssql) _logger.Debug("Registering GDPicture License") If _mssql IsNot Nothing Then Dim oSQL = "SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE'" _gdpictureLicenseKey = _mssql.GetScalarValue(oSQL) Else _logger.Warn("GDPicture License could not be registered! MSSQL is not enabled!") Throw New ArgumentNullException("MSSQL") End If End Sub Private Function MoveAndRenameEmailToRejected(pArgs As WorkerArgs, pMessageId As String) As EmailData _logger.Info("Moving Mail with MessageId [{0}] to Rejected folder", pMessageId) _logger.Debug("Fetching Email Data") Dim oEmailData = _email.GetEmailDataForMessageId(pMessageId) _logger.Debug("Email Data fetched!") Dim oSource = _email.GetOriginalEmailPath(pArgs.OriginalEmailDirectory, pMessageId) _logger.Debug("Original email path: [{0}]", oSource) Dim oDateSubDirectoryName As String = Now.ToString("yyyy-MM-dd") Dim oDestination As String Dim oRejectedDirectory As String = Path.Combine(pArgs.RejectedEmailDirectory, oDateSubDirectoryName) ' Create the destination directory if it does not exist _logger.Debug("Creating destination directory [{0}]", oRejectedDirectory) If Not Directory.Exists(oRejectedDirectory) Then Try Directory.CreateDirectory(oRejectedDirectory) Catch ex As Exception _logger.Error(ex) End Try End If If oSource = String.Empty Then _logger.Warn("Original Email for [{0}] could not be found. Exiting.", pMessageId) Return Nothing End If ' If oEmailData is Nothing, TBEDM_EMAIL_PROFILER_HISTORY for MessageId was not found. ' This only should happen when testing and db-tables are deleted frequently If oEmailData Is Nothing Then oDestination = _email.GetEmailPathWithSubjectAsName(oRejectedDirectory, pMessageId) Else oDestination = _email.GetEmailPathWithSubjectAsName(oRejectedDirectory, StringEx.ConvertTextToSlug(oEmailData.Subject)) End If _logger.Debug("Destination for eml file is {0}", oDestination) Dim oFinalFileName = _filesystem.GetVersionedFilename(oDestination) _logger.Debug("Versioned filename for eml file is {0}", oFinalFileName) If oEmailData Is Nothing Then _logger.Warn("Could not get Email Data from firebird-database. File {0} will not be moved!", oSource) Return Nothing End If '--------------------------- Try _logger.Info("Moving email from {0} to {1}", oSource, oFinalFileName) File.Move(oSource, oFinalFileName) oEmailData.Attachment = oFinalFileName Catch ex As Exception _logger.Warn("File {0} could not be moved! Original Filename will be used!", oSource) _logger.Error(ex) oEmailData.Attachment = oSource End Try _logger.Info("Email [{0}] moved to rejected folder!", pMessageId) Return oEmailData End Function Private Sub AddRejectedState(oMessageID As String, oTitle As String, oTitle1 As String, oComment As String, Transaction As SqlTransaction) Try 'PRCUST_ADD_HISTORY_STATE: @MessageID VARCHAR(250), @TITLE1 VARCHAR(250), @TITLE2 VARCHAR(250) Dim oSQL = $"EXEC PRCUST_ADD_HISTORY_STATE '{oMessageID}','{oTitle}','{oTitle1}','{oComment.Replace("'", "''")}'" _mssql.ExecuteNonQuery(oSQL, Transaction) Catch ex As Exception _logger.Error(ex) End Try End Sub Public Sub Start(Arguments As Object) Implements IJob.Start Dim oArgs As WorkerArgs = Arguments Dim oPropertyExtractor = New PropertyValues(_logConfig) Dim oAttachmentExtractor = New PDFEmbeds(_logConfig) _EmailOutAccountId = oArgs.EmailOutProfileId Dim oOptions As New ZUGFeRDInterface.ZugferdOptions() With { .AllowFacturX_Filename = oArgs.AllowFacturX, .AllowXRechnung_Filename = oArgs.AllowXRechnung } _zugferd = New ZUGFeRDInterface(_logConfig, _gdpictureLicenseKey, oOptions) _logger.Debug("Starting Job {0}", [GetType].Name) Try For Each oPath As String In oArgs.WatchDirectories Dim oDirInfo As New DirectoryInfo(oPath) _logger.Debug($"Start processing directory {oDirInfo.FullName}") If oDirInfo.Exists Then ' Filter out *.lock files Dim oFiles As List(Of FileInfo) = oDirInfo. GetFiles(). Where(Function(f) Not f.Name.EndsWith(".lock")). ToList() Dim oFileCount = oFiles.Count Dim oCurrentFileCount = 0 If oFileCount = 0 Then _logger.Debug("No files to process.") Continue For Else _logger.Info("Found {0} files", oFileCount) End If ' Group files by messageId Dim oGrouped As Dictionary(Of String, List(Of FileInfo)) = _zugferd.FileGroup.GroupFiles(oFiles) _logger.Info("Found {0} file groups", oGrouped.Count) ' Process each file group together For Each oFileGroup In oGrouped ' Start a new transaction for each file group. ' This way we can rollback database changes for the whole filegroup in case something goes wrong. Dim oSQLConnection As SqlConnection = _mssql.GetConnection() Dim oSQLTransaction As SqlTransaction = oSQLConnection?.BeginTransaction() Dim oConnections As New DatabaseConnections() With { .SQLServerConnection = oSQLConnection, .SQLServerTransaction = oSQLTransaction } ' Count the amount of ZUGFeRD files Dim oZUGFeRDCount As Integer = 0 ' Set the default Move Directory Dim oMoveDirectory As String = oArgs.ErrorDirectory ' Flag to save if the whole process was a success. ' Will be set only at the end of the function if no error occurred. Dim oIsSuccess As Boolean = False ' Flag to save if the occurred error (if any) was expected ' Used to determine if transactions should be committed or not Dim oExpectedError As Boolean = True ' Create file lists Dim oFileGroupFiles As List(Of FileInfo) = oFileGroup.Value Dim oEmailAttachmentFiles As New List(Of FileInfo) Dim oEmbeddedAttachmentFiles As New List(Of PDFEmbeds.EmbeddedFile) Dim oMessageId As String = oFileGroup.Key Dim oMissingProperties As New List(Of String) Dim oMD5CheckSum As String = String.Empty _logger.Info("Start processing file group {0}", oMessageId) Try For Each oFile In oFileGroupFiles ' 09.12.2021: oDocument is now an Object, because have different classes corresponding to the ' different versions of ZUGFeRD and the type is unknown at compile-time. ' 17.11.2022: oDocument is now a Tuple of (String, Object), to be able to return the filename ' of the extracted xml file. ' 21.12.2022: oDocument is now an object of type ZugferdResult to be able to save ' the new meta data, ie. the type of schema (zugferd version) Dim oDocument As ZUGFeRDInterface.ZugferdResult ' Start a global group counter for each file Dim oGlobalGroupCounter = 0 ' Clear missing properties for the new file oMissingProperties = New List(Of String) oCurrentFileCount += 1 ' Only pdf files are allowed from here on If Not oFile.Name.ToUpper.EndsWith(".PDF") Then _logger.Debug("Skipping non-pdf file {0}", oFile.Name) oEmailAttachmentFiles.Add(oFile) ' Checking filesize for attachment files If _filesystem.TestFileSizeIsLessThanMaxFileSize(oFile.FullName, oArgs.MaxAttachmentSizeInMegaBytes) = False Then _logger.Warn("Filesize for File [{0}] exceeded limit of {1} MB", oFile.Name, oArgs.MaxAttachmentSizeInMegaBytes) Throw New FileSizeLimitReachedException(oFile.Name, oArgs.MaxAttachmentSizeInMegaBytes) End If Continue For End If _logger.Info("Start processing file {0}", oFile.Name) ' Checking filesize for pdf files If _filesystem.TestFileSizeIsLessThanMaxFileSize(oFile.FullName, oArgs.MaxAttachmentSizeInMegaBytes) = False Then _logger.Warn("Filesize for File [{0}] exceeded limit of {1} MB", oFile.Name, oArgs.MaxAttachmentSizeInMegaBytes) Throw New FileSizeLimitReachedException(oFile.Name, oArgs.MaxAttachmentSizeInMegaBytes) End If Try oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName) Catch ex As ValidationException Throw ex Catch ex As ZUGFeRDExecption Select Case ex.ErrorType Case ZUGFeRDInterface.ErrorType.NoZugferd _logger.Info("File [{0}] is not a valid ZUGFeRD document. Skipping.", oFile.Name) oEmailAttachmentFiles.Add(oFile) Continue For Case ZUGFeRDInterface.ErrorType.UnsupportedFormat _logger.Info("File [{0}/{1}] is an unsupported ZUFeRD document format!", oFile.Name, ex.XmlFile) Throw New UnsupportedFerdException(ex.XmlFile) Case ZUGFeRDInterface.ErrorType.NoValidZugferd _logger.Info("File [{0}] is an Incorrectly formatted ZUGFeRD document!", oFile.Name) Throw New InvalidFerdException() Case Else _logger.Warn("Unexpected Error occurred while extracting ZUGFeRD Information from file {0}", oFile.Name) Throw ex End Select End Try ' Check if there are more than one ZUGFeRD files If oZUGFeRDCount = 1 Then Throw New TooMuchFerdsException() End If ' Since extraction went well, increase the amount of ZUGFeRD files oZUGFeRDCount += 1 ' Extract all attachments with the extensions specified in `AllowedExtensions`. ' If you need to extract and use embedded xml files, you need to filter out the zugferd-invoice.xml yourself. ' Right now the zugferd-invoice.xml is filtered out because `AllowedExtensions` does not contain `xml`. Dim oAttachments = oAttachmentExtractor.Extract(oFile.FullName, AllowedExtensions) If oAttachments Is Nothing Then _logger.Warn("Attachments for file [{0}] could not be extracted", oFile.FullName) Else oEmbeddedAttachmentFiles.AddRange(oAttachments) End If ' Check the Checksum and rejection status oMD5CheckSum = GenerateAndCheck_MD5Sum(oFile.FullName, oMessageId, oArgs.IgnoreRejectionStatus) ' Check the document against the configured property map and return: ' - a List of valid properties ' - a List of missing properties Dim oPropertyMap = _zugferd.FilterPropertyMap(oArgs.PropertyMap, oDocument.Specification) Dim oCheckResult = _zugferd.PropertyValues.CheckPropertyValues(oDocument.SchemaObject, oPropertyMap, oMessageId) _logger.Info("Properties checked: [{0}] missing properties / [{1}] valid properties found.", oCheckResult.MissingProperties.Count, oCheckResult.ValidProperties.Count) If oCheckResult.MissingProperties.Count > 0 Then _logger.Warn("[{0}] missing properties found. Exiting.", oCheckResult.MissingProperties.Count) oMissingProperties = oCheckResult.MissingProperties Throw New MissingValueException(oFile) Else _logger.Debug("No missing properties found. Continuing.") End If DeleteExistingPropertyValues(oMessageId, oConnections) Dim oFirstProperty = oCheckResult.ValidProperties.FirstOrDefault() If oFirstProperty IsNot Nothing Then InsertPropertyValue(oMessageId, oConnections, New PropertyValues.ValidProperty() With { .MessageId = oMessageId, .Description = "ZUGFeRDSpezifikation", .GroupCounter = 0, .IsRequired = False, .Value = oDocument.Specification, .TableName = oFirstProperty.TableName, .TableColumn = "ZUGFERD_SPECIFICATION" }) End If For Each oProperty In oCheckResult.ValidProperties InsertPropertyValue(oMessageId, oConnections, oProperty) Next Next 'Check if there are no ZUGFeRD files If oZUGFeRDCount = 0 Then ' If NonZugferdDirectory is not set, a NoFerdsException will be thrown and a rejection will be generated ' This is the default/initial behaviour. If oArgs.NonZugferdDirectory Is Nothing OrElse oArgs.NonZugferdDirectory = String.Empty Then Throw New NoFerdsException() End If ' Also, if the directory is set but does not exist, still a rejection will be generated. If Not IO.Directory.Exists(oArgs.NonZugferdDirectory) Then Throw New NoFerdsException() End If ' Only if the directory is set and does exist, it will be used and any file groups which ' do NOT CONTAIN ANY ZUGFERD DOCUMENTS, are moved to that directory. Throw New NoFerdsAlternateException() End If 'If no errors occurred... 'Log the History If oMD5CheckSum <> String.Empty Then _history.Create_HistoryEntry(oMessageId, oMD5CheckSum, "SUCCESS", oSQLTransaction) Else _history.Create_HistoryEntry(oMessageId, String.Empty, "SUCCESS (with empty MD5Hash)", oSQLTransaction) End If oIsSuccess = True oMoveDirectory = oArgs.SuccessDirectory Catch ex As ValidationException _logger.Error(ex) Dim oErrors = ex.ValidationErrors Dim oMessage = "REJECTED - ZUGFeRD yes but formal validation failed!" _history.Update_HistoryEntry(oMessageId, oMD5CheckSum, oMessage, oSQLTransaction) Dim oErrorList As String = "" For Each oError In oErrors oErrorList += $"