Skip to content

Annytab Dox Exchange Standard

Annytab Dox Exchange Standard is a royalty-free and distributed system to exchange electronic documents (e-documents). An e-document is exchanged as a zip-file (attachment) in an e-mail, the zip-file contains a meta-data file and the actual file. Distribution of electronic documents through emails makes the system open and free of charges, no one owns the distribution system.

An e-document should be zipped together with a meta.json file, metadata includes information about date, encoding, language and signatures. The zip-fil should end with a dox.zip extension. The contents of the meta.json file is specified below, this specification can be updated in the future and your code must be able to handle missing values (null). We are also developing standardized e-documents that can be exchanged in this system. This project exists on GitHub (a-dox-standards) and is also available as a NuGet package.

This exchange system can be automatic or manual. A zip-file is sent as an attachment in an e-mail to one or more recepients, a recepient can pickup attachments from the inbox and process electronic documents received. Example code is provided at the end of this post, visit the project on GitHub to view all code for the example.

AnnytabDoxMeta [Model]

Property
Type
Description
date_of_sending
string
The date when the file was sent. The date should be entered as yyyy-MM-dd (2017-09-31).
file_encoding
string
Specifies how a file has been encoded, specified for text files (txt, xml, json) so that the recipient can convert the file to a string. Possible values are ASCII, UTF-8, UTF-16 or UTF-32.
filename
string
A local name for the file, the file extension is used to determine the file’s media type (MIME).
standard_name
string
The name of the standard used to create the file. Used by the recipient to determine how the file should be processed.
language_code
string
A 2-letter code according to ISO 639-1 that specifies the language used in the file. Applies primarily to text files (txt, xml, json) and indicates to the recipient how the file can be translated.
signatures
IList<Signature>
A list of electronic signatures concerning the file. See the model for Signature below.

Signature [Model]

Property
Type
Description
validation_type
string
A value that specifies the type of signature that has been applied. Is used as help to validate the signature.
algorithm
string
The hashalgorithm applied to the signature (SHA-1, SHA-256, SHA-384 or SHA-512). A hash algorithm is a cryptographic hash function that produces a hash value based on underlying data.
padding
string
The type of padding applied to the signature (Pkcs1 or Pss). Random length padding makes it harder to identify plain text in signed data.
data
string
The underlying data for the signature (email address, current date, MD5 hash of the file), each part is delimited by comma (,) without spaces (info@annytab.se,2017-09-08,4UKmkFdgur/NxmZzV2CvpA==). Binds the signature to email, date and file. This data is used when the signature is verified.
value
string
A signature hash value encoded as a Base64 string, no BEGIN or END headers should be specified.
certificate
string
The public certificate of the signature encoded as a Base64 string, no BEGIN or END headers should be specified. This certificate is required to verify the signature.

JSON Example

  1. {
  2. "date_of_sending": "2020-04-17",
  3. "file_encoding": "utf-8",
  4. "filename": "invoice_D1005.json",
  5. "standard_name": "Annytab Dox Trade v1",
  6. "language_code": "en",
  7. "signatures": [
  8. {
  9. "validation_type": "eID Smart Card",
  10. "algorithm": "SHA-256",
  11. "padding": "Pkcs1",
  12. "data": "invoice@annytab.se,2018-10-30,8RkVQp7KTlbTLiBV6wLJag==",
  13. "value": "HK8Cv/KRhvffPna...=",
  14. "certificate": "MIIFTzCCAzegAwIBAgIQ..."
  15. },
  16. {
  17. "validation_type": "BankID v5",
  18. "algorithm": "",
  19. "padding": "",
  20. "data": "fredde@jfsbokforing.se,2018-09-13,Ar/so6msWR4av3nAfw9GcQ==",
  21. "value": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVV...",
  22. "certificate": ""
  23. }
  24. ]
  25. }

Code Example

  1. using System;
  2. using System.IO;
  3. using System.IO.Compression;
  4. using System.Collections.Generic;
  5. using System.Text;
  6. using System.Net;
  7. using System.Net.Mail;
  8. using System.Threading.Tasks;
  9. using Microsoft.Extensions.Configuration;
  10. using Microsoft.Extensions.Logging;
  11. using Microsoft.Extensions.DependencyInjection;
  12. using Microsoft.VisualStudio.TestTools.UnitTesting;
  13. using Newtonsoft.Json;
  14. using Annytab.Dox.Standards.V1;
  15. namespace TestProgram
  16. {
  17. [TestClass]
  18. public class FilesTest
  19. {
  20. #region Variables
  21. private IConfigurationRoot configuration { get; set; }
  22. private ILogger logger { get; set; }
  23. private EmailOptions options { get; set; }
  24. #endregion
  25. #region Constructors
  26. /// <summary>
  27. /// Create a new test instance
  28. /// </summary>
  29. public FilesTest()
  30. {
  31. // Add configuration settings
  32. ConfigurationBuilder builder = new ConfigurationBuilder();
  33. builder.SetBasePath(Directory.GetCurrentDirectory());
  34. builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
  35. builder.AddJsonFile($"appsettings.Development.json", optional: true);
  36. this.configuration = builder.Build();
  37. // Create a service collection
  38. IServiceCollection services = new ServiceCollection();
  39. // Add logging and options as services
  40. services.AddLogging(logging => {
  41. logging.AddConfiguration(configuration.GetSection("Logging"));
  42. logging.AddConsole();
  43. logging.AddDebug();
  44. });
  45. services.AddOptions();
  46. // Get options
  47. this.options = configuration.GetSection("EmailOptions").Get<EmailOptions>();
  48. // Build a service provider
  49. IServiceProvider serviceProvider = services.BuildServiceProvider();
  50. // Configure file logging
  51. ILoggerFactory loggerFactory = serviceProvider.GetService<ILoggerFactory>();
  52. loggerFactory.AddFile("C:\\DATA\\home\\AnnytabDoxStandards\\Logs\\log-{Date}.txt");
  53. // Get references
  54. this.logger = loggerFactory.CreateLogger<FilesTest>();
  55. } // End of the constructor
  56. #endregion
  57. [TestMethod]
  58. public void SaveToDisk()
  59. {
  60. // Create a file path
  61. string path = "C:\\DATA\\home\\AnnytabDoxStandards\\" + Guid.NewGuid().ToString() + ".dox.zip";
  62. // Create documents
  63. IDictionary<string, byte[]> files = new Dictionary<string, byte[]>();
  64. files.Add("meta.json", Documents.CreateAnnytabDoxMeta());
  65. files.Add("file.json", Documents.CreateAnnytabDoxInvoice());
  66. // Create and use a new file stream
  67. using(FileStream zip = new FileStream(path, FileMode.Create))
  68. {
  69. // Create and use a new zip archive
  70. using (ZipArchive archive = new ZipArchive(zip, ZipArchiveMode.Create, true))
  71. {
  72. // Add files to archive
  73. foreach (KeyValuePair<string, byte[]> file in files)
  74. {
  75. // Add the file to the zip
  76. ZipArchiveEntry entry = archive.CreateEntry(file.Key, CompressionLevel.Fastest);
  77. using (Stream stream = entry.Open())
  78. {
  79. stream.Write(file.Value, 0, file.Value.Length);
  80. }
  81. }
  82. }
  83. }
  84. } // End of the SaveToDisk method
  85. [TestMethod]
  86. public void ReadFromDisk()
  87. {
  88. // Create a file path
  89. string directory = "C:\\DATA\\home\\AnnytabDoxStandards";
  90. // Variables
  91. AnnytabDoxMeta meta = null;
  92. byte[] file_array = null;
  93. // Get all files
  94. string[] files = System.IO.Directory.GetFiles(directory + "\\Open");
  95. // Loop files
  96. foreach (string path in files)
  97. {
  98. // Create and use an archive
  99. using (ZipArchive archive = ZipFile.OpenRead(path))
  100. {
  101. // Loop files in zip
  102. foreach (ZipArchiveEntry entry in archive.Entries)
  103. {
  104. // Check if a file is meta or file
  105. if (entry.FullName.StartsWith("meta", StringComparison.OrdinalIgnoreCase))
  106. {
  107. using (MemoryStream stream = new MemoryStream())
  108. {
  109. entry.Open().CopyTo(stream);
  110. byte[] array = stream.ToArray();
  111. meta = JsonConvert.DeserializeObject<AnnytabDoxMeta>(Encoding.UTF8.GetString(array, 0, array.Length));
  112. }
  113. }
  114. else
  115. {
  116. using (MemoryStream stream = new MemoryStream())
  117. {
  118. entry.Open().CopyTo(stream);
  119. file_array = stream.ToArray();
  120. }
  121. }
  122. }
  123. // Log standard name
  124. this.logger.LogInformation($"Fetching: {meta.standard_name} from open folder.", null);
  125. // Get file contents depending on standard name
  126. if (meta.standard_name.Equals("Annytab Dox Trade v1", StringComparison.OrdinalIgnoreCase))
  127. {
  128. AnnytabDoxTrade doc = JsonConvert.DeserializeObject<AnnytabDoxTrade>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
  129. }
  130. else if (meta.standard_name.Equals("Annytab Dox Contract v1", StringComparison.OrdinalIgnoreCase))
  131. {
  132. AnnytabDoxContract doc = JsonConvert.DeserializeObject<AnnytabDoxContract>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
  133. }
  134. else if (meta.standard_name.Equals("Annytab Dox Drive Log v1", StringComparison.OrdinalIgnoreCase))
  135. {
  136. AnnytabDoxDriveLog doc = JsonConvert.DeserializeObject<AnnytabDoxDriveLog>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
  137. }
  138. else if (meta.standard_name.Equals("Annytab Dox Travel Expense Claim v1", StringComparison.OrdinalIgnoreCase))
  139. {
  140. AnnytabDoxTravelExpenseClaim doc = JsonConvert.DeserializeObject<AnnytabDoxTravelExpenseClaim>(Encoding.UTF8.GetString(file_array, 0, file_array.Length));
  141. }
  142. }
  143. // Move file from open to closed
  144. System.IO.Directory.Move(path, directory + "\\Closed\\" + Path.GetFileName(path));
  145. }
  146. } // End of the ReadFromDisk method
  147. [TestMethod]
  148. public async Task SendEmail()
  149. {
  150. // Create documents
  151. IDictionary<string, byte[]> files = new Dictionary<string, byte[]>();
  152. files.Add("meta.json", Documents.CreateAnnytabDoxMeta());
  153. files.Add("file.json", Documents.CreateAnnytabDoxInvoice());
  154. // Create and use a new memory stream
  155. using (MemoryStream zip = new MemoryStream())
  156. {
  157. // Create and use a new zip archive
  158. using (ZipArchive archive = new ZipArchive(zip, ZipArchiveMode.Create, true))
  159. {
  160. // Add files to archive
  161. foreach (KeyValuePair<string, byte[]> file in files)
  162. {
  163. // Add the file to zip
  164. ZipArchiveEntry entry = archive.CreateEntry(file.Key, CompressionLevel.Fastest);
  165. using (Stream stream = entry.Open())
  166. {
  167. stream.Write(file.Value, 0, file.Value.Length);
  168. }
  169. }
  170. }
  171. // Move the pointer to the start of the stream
  172. zip.Seek(0, SeekOrigin.Begin);
  173. // Create an smtp client
  174. SmtpClient smtp = new SmtpClient(this.options.Host, this.options.Port.GetValueOrDefault());
  175. smtp.Credentials = new NetworkCredential(this.options.Email, this.options.Password);
  176. smtp.EnableSsl = true;
  177. // Try to send the email message
  178. try
  179. {
  180. // Create a mail message
  181. MailMessage message = new MailMessage(this.options.Email, this.options.Pickup);
  182. // Create the mail message
  183. message.Subject = "Sending file";
  184. message.Body = "File is attached.";
  185. message.IsBodyHtml = true;
  186. // Add an attachment
  187. if (zip != null)
  188. {
  189. Attachment attach = new Attachment(zip, new System.Net.Mime.ContentType("application/zip"));
  190. attach.ContentDisposition.FileName = Guid.NewGuid().ToString() + ".dox.zip";
  191. message.Attachments.Add(attach);
  192. }
  193. // Send the mail message
  194. await smtp.SendMailAsync(message);
  195. }
  196. catch (Exception ex)
  197. {
  198. // Log the exception
  199. this.logger.LogError(ex, $"Send email to: {this.options.Pickup}", null);
  200. }
  201. }
  202. } // End of the SendEmail method
  203. [TestMethod]
  204. public void PickupEmails()
  205. {
  206. // Reference to a directory
  207. string directory = "C:\\DATA\\home\\AnnytabDoxStandards\\Open\\";
  208. // Create and use an imap client
  209. using (var client = new MailKit.Net.Imap.ImapClient())
  210. {
  211. // Add credentials
  212. client.Connect(this.options.Host, 993, true);
  213. client.Authenticate(this.options.Pickup, this.options.Password);
  214. // Get inbox folder
  215. MailKit.IMailFolder inbox = client.Inbox;
  216. inbox.Open(MailKit.FolderAccess.ReadWrite);
  217. // Write information about the inbox
  218. Console.WriteLine("Total messages: {0}", inbox.Count);
  219. Console.WriteLine("Recent messages: {0}", inbox.Recent);
  220. // Create a query
  221. MailKit.Search.SearchQuery query = MailKit.Search.SearchQuery.NotDeleted.And(MailKit.Search.SearchQuery.NotSeen);
  222. // Loop messages
  223. foreach(MailKit.UniqueId uid in inbox.Search(query))
  224. {
  225. // Get the message
  226. var message = inbox.GetMessage(uid);
  227. // Print subject
  228. Console.WriteLine("Subject: {0}", message.Subject);
  229. // Get attachments
  230. foreach (var attachment in message.Attachments)
  231. {
  232. // Get information about the attachment
  233. var file_name = attachment.ContentDisposition?.FileName;
  234. file_name = string.IsNullOrEmpty(file_name) ? Guid.NewGuid().ToString() + ".noname" : Guid.NewGuid().ToString() + Tools.GetExtensions(file_name);
  235. var content_type = attachment.ContentType;
  236. // Only accept files that ends with dox.zip
  237. if(file_name.EndsWith(".dox.zip") == true)
  238. {
  239. // Save the attachment to disk
  240. using (FileStream stream = new FileStream(directory + file_name, FileMode.Create))
  241. {
  242. if (attachment is MimeKit.MessagePart)
  243. {
  244. var part = (MimeKit.MessagePart)attachment;
  245. part.Message.WriteTo(stream);
  246. }
  247. else
  248. {
  249. var part = (MimeKit.MimePart)attachment;
  250. part.Content.DecodeTo(stream);
  251. }
  252. }
  253. }
  254. }
  255. // Flag email as seen
  256. inbox.AddFlags(uid, MailKit.MessageFlags.Seen, true);
  257. //inbox.AddFlags(uid, MailKit.MessageFlags.Deleted, true);
  258. }
  259. // Disconnect client
  260. client.Disconnect(true);
  261. }
  262. } // End of the PickupEmails method
  263. } // End of the class
  264. } // End of the namespace