Annytab Dox Exchange Standard

Annytab Dox Exchange Standard är ett royaltyfritt och distribuerat system för utbyte av elektroniska dokument (e-dokument). Ett e-dokument skickas som en zip-fil (bilaga) i ett e-postmeddelande, zip-filen innehåller en metadatafil och den faktiska filen. Distribution av elektroniska dokument via e-post gör systemet öppet och kostnadsfritt, ingen äger distributionssystemet.

Ett e-dokument ska zippas tillsammans med en meta.json-fil, metadatan innehåller information om datum, kodning, språk och signaturer. ZIP-filen skall ha dox.zip som filändelse. Innehållet i filen meta.json anges nedan, denna specifikation kan uppdateras i framtiden och din kod måste kunna hantera ej angivna värden (null). Vi utvecklar också standardiserade e-dokument som kan utbytas i detta system. Detta projekt finns på GitHub (a-dox-standards) och som ett NuGet-paket.

Detta växlingssystem kan hanteras automatiskt eller manuellt. En zip-fil skickas som en bilaga i ett e-postmeddelande till en eller flera mottagare, en mottagare kan hämta bilagor från inkorgen och behandla mottagna elektroniska dokument. Exempelkod tillhandahålls i slutet av detta inlägg, besök projektet på GitHub för att se all kod för exemplet.

AnnytabDoxMeta [Modell]

Egenskap
Typ
Beskrivning
date_of_sending
string
Datumet då filen skickades. Datument skall anges som yyyy-MM-dd (2017-09-31).
file_encoding
string
Anger hur en fil har kodats, anges för textfiler (txt, xml, json) så att mottagaren kan konvertera filen till en sträng. Möjliga värden är ASCII, UTF-8, UTF-16 eller UTF-32.
filename
string
Ett lokalt filnamn för filen, filtillägget används för att bestämma filens mediatyp (MIME).
standard_name
string
Namnet på den standard som har använts för skapa filen. Används av mottagaren för att avgöra hur filen skall behandlas.
language_code
string
En kod om 2 tecken enligt ISO 639-1 som anger vilket språk som tillämpas i filen. Tillämpas främst för textfiler (txt, xml, json) och indikerar för mottagaren hur filen skall översättas.
signatures
IList<Signature>
En lista med elektroniska signaturer avseende filen. Se modellen för Signature nedanför.

Signatur [Modell]

Egenskap
Typ
Beskrivning
validation_type
string
Ett värde som anger vilken typ av signatur som har tillämpats. Används som hjälp vid validering av signaturen.
algorithm
string
Den hashalgorithm som tillämpas för signaturen (SHA-1, SHA-256, SHA-384 eller SHA-512). En hashalgoritm är en kryptografisk hashfunktion som producerar ett hash-värde utifrån underliggande data.
padding
string
Den typ av utfyllnad som tillämpas för signaturen (Pkcs1 eller Pss). Slumpmässig längdutfyllnad gör det svårare att identifiera ren text i signerad data.
data
string
Underliggande data för signaturen (e-postadress,dagens datum,MD5-hash av filen), varje del separeras med komma (,) utan mellanslag. Binder signaturen till e-post, datum och fil. Denna data används när signaturen skall verifieras.
value
string
Signaturens hash-värde som en Base64-sträng, inga BEGIN eller END headers skall anges.
certificate
string
Det publika certifikatet för signaturen som en Base64-sträng, inga BEGIN eller END headers skall anges. Detta certifikat behövs för att kunna verifiera signaturen.

JSON-Exempel

{
  "date_of_sending": "2020-04-17",
  "file_encoding": "utf-8",
  "filename": "invoice_D1005.json",
  "standard_name": "Annytab Dox Trade v1",
  "language_code": "en",
  "signatures": [
    {
      "validation_type": "eID Smart Card",
      "algorithm": "SHA-256",
      "padding": "Pkcs1",
      "data": "invoice@annytab.se,2018-10-30,8RkVQp7KTlbTLiBV6wLJag==",
      "value": "HK8Cv/KRhvffPna7E...",
      "certificate": "MIIFTzCCAzegAwIBAgIQE3..."
    },
    {
      "validation_type": "BankID v5",
      "algorithm": "",
      "padding": "",
      "data": "fredde@jfsbokforing.se,2018-09-13,Ar/so6msWR4av3nAfw9GcQ==",
      "value": "PD94bWwgdmVyc2lvbj0iM...",
      "certificate": ""
    }
  ]
}

Exempelkod

using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Annytab.Dox.Standards.V1;

namespace TestProgram
{
    [TestClass]
    public class FilesTest
    {
        #region Variables

        private IConfigurationRoot configuration { get; set; }
        private ILogger logger { get; set; }
        private EmailOptions options { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new test instance
        /// </summary>
        public FilesTest()
        {
            // Add configuration settings
            ConfigurationBuilder builder = new ConfigurationBuilder();
            builder.SetBasePath(Directory.GetCurrentDirectory());
            builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
            builder.AddJsonFile($"appsettings.Development.json", optional: true);
            this.configuration = builder.Build();

            // Create a service collection
            IServiceCollection services = new ServiceCollection();

            // Add logging and options as services
            services.AddLogging(logging => {
                logging.AddConfiguration(configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
            });
            services.AddOptions();

            // Get options
            this.options = configuration.GetSection("EmailOptions").Get<EmailOptions>();

            // Build a service provider
            IServiceProvider serviceProvider = services.BuildServiceProvider();

            // Configure file logging
            ILoggerFactory loggerFactory = serviceProvider.GetService<ILoggerFactory>();
            loggerFactory.AddFile("C:\\DATA\\home\\AnnytabDoxStandards\\Logs\\log-{Date}.txt");

            // Get references
            this.logger = loggerFactory.CreateLogger<FilesTest>();

        } // End of the constructor

        #endregion

        [TestMethod]
        public void SaveToDisk()
        {
            // Create a file path
            string path = "C:\\DATA\\home\\AnnytabDoxStandards\\" + Guid.NewGuid().ToString() + ".dox.zip";

            // Create documents
            IDictionary<string, byte[]> files = new Dictionary<string, byte[]>();
            files.Add("meta.json", Documents.CreateAnnytabDoxMeta());
            files.Add("file.json", Documents.CreateAnnytabDoxInvoice());

            // Create and use a new file stream
            using(FileStream zip = new FileStream(path, FileMode.Create))
            {
                // Create and use a new zip archive
                using (ZipArchive archive = new ZipArchive(zip, ZipArchiveMode.Create, true))
                {
                    // Add files to archive
                    foreach (KeyValuePair<string, byte[]> file in files)
                    {
                        // Add the file to the zip
                        ZipArchiveEntry entry = archive.CreateEntry(file.Key, CompressionLevel.Fastest);
                        using (Stream stream = entry.Open())
                        {
                            stream.Write(file.Value, 0, file.Value.Length);
                        }
                    }
                }
            }

        } // End of the SaveToDisk method

        [TestMethod]
        public void ReadFromDisk()
        {
            // Create a file path
            string directory = "C:\\DATA\\home\\AnnytabDoxStandards";

            // Variables
            AnnytabDoxMeta meta = null;
            byte[] file_array = null;

            // Get all files
            string[] files = System.IO.Directory.GetFiles(directory + "\\Open");

            // Loop files
            foreach (string path in files)
            {
                // Create and use an archive
                using (ZipArchive archive = ZipFile.OpenRead(path))
                {
                    // Loop files in zip
                    foreach (ZipArchiveEntry entry in archive.Entries)
                    {
                        // Check if a file is meta or file
                        if (entry.FullName.StartsWith("meta", StringComparison.OrdinalIgnoreCase))
                        {
                            using (MemoryStream stream = new MemoryStream())
                            {
                                entry.Open().CopyTo(stream);
                                byte[] array = stream.ToArray();
                                meta = JsonConvert.DeserializeObject<AnnytabDoxMeta>(Encoding.UTF8.GetString(array, 0, array.Length));
                            }
                        }
                        else
                        {
                            using (MemoryStream stream = new MemoryStream())
                            {
                                entry.Open().CopyTo(stream);
                                file_array = stream.ToArray();
                            }
                        }
                    }

                    // Log standard name
                    this.logger.LogInformation($"Fetching: {meta.standard_name} from open folder.", null);

                    // Get file contents depending on standard name
                    if (meta.standard_name.Equals("Annytab Dox Trade v1", StringComparison.OrdinalIgnoreCase))
                    {
                        AnnytabDoxTrade doc = JsonConvert.DeserializeObject<AnnytabDoxTrade>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
                    }
                    else if (meta.standard_name.Equals("Annytab Dox Contract v1", StringComparison.OrdinalIgnoreCase))
                    {
                        AnnytabDoxContract doc = JsonConvert.DeserializeObject<AnnytabDoxContract>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
                    }
                    else if (meta.standard_name.Equals("Annytab Dox Drive Log v1", StringComparison.OrdinalIgnoreCase))
                    {
                        AnnytabDoxDriveLog doc = JsonConvert.DeserializeObject<AnnytabDoxDriveLog>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
                    }
                    else if (meta.standard_name.Equals("Annytab Dox Travel Expense Claim v1", StringComparison.OrdinalIgnoreCase))
                    {
                        AnnytabDoxTravelExpenseClaim doc = JsonConvert.DeserializeObject<AnnytabDoxTravelExpenseClaim>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
                    }
                }

                // Move file from open to closed
                System.IO.Directory.Move(path, directory + "\\Closed\\" + Path.GetFileName(path));
            }

        } // End of the ReadFromDisk method

        [TestMethod]
        public async Task SendEmail()
        {
            // Create documents
            IDictionary<string, byte[]> files = new Dictionary<string, byte[]>();
            files.Add("meta.json", Documents.CreateAnnytabDoxMeta());
            files.Add("file.json", Documents.CreateAnnytabDoxInvoice());

            // Create and use a new memory stream
            using (MemoryStream zip = new MemoryStream())
            {
                // Create and use a new zip archive
                using (ZipArchive archive = new ZipArchive(zip, ZipArchiveMode.Create, true))
                {
                    // Add files to archive
                    foreach (KeyValuePair<string, byte[]> file in files)
                    {
                        // Add the file to zip
                        ZipArchiveEntry entry = archive.CreateEntry(file.Key, CompressionLevel.Fastest);
                        using (Stream stream = entry.Open())
                        {
                            stream.Write(file.Value, 0, file.Value.Length);
                        }
                    }
                }

                // Move the pointer to the start of the stream
                zip.Seek(0, SeekOrigin.Begin);

                // Create an smtp client
                SmtpClient smtp = new SmtpClient(this.options.Host, this.options.Port.GetValueOrDefault());
                smtp.Credentials = new NetworkCredential(this.options.Email, this.options.Password);
                smtp.EnableSsl = true;

                // Try to send the email message
                try
                {
                    // Create a mail message
                    MailMessage message = new MailMessage(this.options.Email, this.options.Pickup);

                    // Create the mail message
                    message.Subject = "Sending file";
                    message.Body = "File is attached.";
                    message.IsBodyHtml = true;

                    // Add an attachment
                    if (zip != null)
                    {
                        Attachment attach = new Attachment(zip, new System.Net.Mime.ContentType("application/zip"));
                        attach.ContentDisposition.FileName = Guid.NewGuid().ToString() + ".dox.zip";
                        message.Attachments.Add(attach);
                    }

                    // Send the mail message
                    await smtp.SendMailAsync(message);
                }
                catch (Exception ex)
                {
                    // Log the exception
                    this.logger.LogError(ex, $"Send email to: {this.options.Pickup}", null);
                }
            }

        } // End of the SendEmail method

        [TestMethod]
        public void PickupEmails()
        {
            // Reference to a directory
            string directory = "C:\\DATA\\home\\AnnytabDoxStandards\\Open\\";

            // Create and use an imap client
            using (var client = new MailKit.Net.Imap.ImapClient())
            {
                // Add credentials
                client.Connect(this.options.Host, 993, true);
                client.Authenticate(this.options.Pickup, this.options.Password);

                // Get inbox folder
                MailKit.IMailFolder inbox = client.Inbox;
                inbox.Open(MailKit.FolderAccess.ReadWrite);

                // Write information about the inbox
                Console.WriteLine("Total messages: {0}", inbox.Count);
                Console.WriteLine("Recent messages: {0}", inbox.Recent);

                // Create a query
                MailKit.Search.SearchQuery query = MailKit.Search.SearchQuery.NotDeleted.And(MailKit.Search.SearchQuery.NotSeen);

                // Loop messages
                foreach(MailKit.UniqueId uid in inbox.Search(query))
                {
                    // Get the message
                    var message = inbox.GetMessage(uid);

                    // Print subject
                    Console.WriteLine("Subject: {0}", message.Subject);

                    // Get attachments
                    foreach (var attachment in message.Attachments)
                    {
                        // Get information about the attachment
                        var file_name = attachment.ContentDisposition?.FileName;
                        file_name = string.IsNullOrEmpty(file_name) ? Guid.NewGuid().ToString() + ".noname" : Guid.NewGuid().ToString() + Tools.GetExtensions(file_name);
                        var content_type = attachment.ContentType;

                        // Only accept files that ends with dox.zip
                        if(file_name.EndsWith(".dox.zip") == true)
                        {
                            // Save the attachment to disk
                            using (FileStream stream = new FileStream(directory + file_name, FileMode.Create))
                            {
                                if (attachment is MimeKit.MessagePart)
                                {
                                    var part = (MimeKit.MessagePart)attachment;
                                    part.Message.WriteTo(stream);
                                }
                                else
                                {
                                    var part = (MimeKit.MimePart)attachment;
                                    part.Content.DecodeTo(stream);
                                }
                            }
                        }               
                    }

                    // Flag email as seen
                    inbox.AddFlags(uid, MailKit.MessageFlags.Seen, true);
                    //inbox.AddFlags(uid, MailKit.MessageFlags.Deleted, true);
                }

                // Disconnect client
                client.Disconnect(true);
            }

        } // End of the PickupEmails method

    } // End of the class

} // End of the namespace