using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
namespace BulkArchiveMailer.Core
{
[DataContract]
public sealed class TemplateData
{
[DataMember(Name = "subject")]
public string Subject { get; set; } = "";
[DataMember(Name = "bodyHtml")]
public string BodyHtml { get; set; } = "";
}
[DataContract]
public sealed class TemplateSetData
{
[DataMember(Name = "ssh_access")]
public TemplateData SshAccess { get; set; } = new TemplateData();
[DataMember(Name = "config_update")]
public TemplateData ConfigUpdate { get; set; } = new TemplateData();
}
/// Shape of %APPDATA%\BulkArchiveMailer\config.json (and of the embedded config.default.json that seeds it).
[DataContract]
public sealed class AppSettingsData
{
[DataMember(Name = "archiveFolder")]
public string ArchiveFolder { get; set; } = "";
[DataMember(Name = "emailDomain")]
public string EmailDomain { get; set; } = "redlab.lo";
/// Whether "test mode" (save to Drafts instead of Send) should be pre-checked when PreviewForm opens. Defaults to true - sending live is an explicit opt-in each time the operator unchecks it.
[DataMember(Name = "testModeDefault")]
public bool TestModeDefault { get; set; } = true;
/// Manual override for the Outlook signature .htm path, used when SignatureReader's registry-based auto-detection fails or the operator wants a different signature. Empty = rely on auto-detection.
[DataMember(Name = "signatureHtmlPath")]
public string SignatureHtmlPath { get; set; } = "";
[DataMember(Name = "templates")]
public TemplateSetData Templates { get; set; } = new TemplateSetData();
public MailTemplateSet ToTemplateSet()
{
return new MailTemplateSet
{
SshAccess = new MailTemplate
{
Subject = Templates?.SshAccess?.Subject ?? "",
BodyHtml = Templates?.SshAccess?.BodyHtml ?? "",
},
ConfigUpdate = new MailTemplate
{
Subject = Templates?.ConfigUpdate?.Subject ?? "",
BodyHtml = Templates?.ConfigUpdate?.BodyHtml ?? "",
},
};
}
public void SetTemplate(ArchiveKind kind, MailTemplate template)
{
Templates = Templates ?? new TemplateSetData();
var data = new TemplateData { Subject = template?.Subject ?? "", BodyHtml = template?.BodyHtml ?? "" };
if (kind == ArchiveKind.SshAccess)
{
Templates.SshAccess = data;
}
else
{
Templates.ConfigUpdate = data;
}
}
}
///
/// Loads/saves the small JSON settings file at
/// %APPDATA%\BulkArchiveMailer\config.json. On first run (file
/// missing) the file is seeded verbatim from the config.default.json
/// resource embedded in this assembly, so the shipped defaults and the
/// initial user-editable file always start out identical - there is
/// only one place (config.default.json) where default copy/domain
/// text is authored.
///
/// Uses DataContractJsonSerializer (part of the .NET Framework since
/// 3.5, ships in System.Runtime.Serialization.dll) rather than a
/// third-party JSON package, specifically so this project has zero
/// NuGet dependencies to resolve - this whole solution cannot be
/// build-tested in the environment this was authored in, so removing
/// every avoidable source of "does the package restore correctly on
/// first open" risk is worth the small amount of extra code below.
///
public static class AppSettings
{
private const string EmbeddedDefaultResourceName = "BulkArchiveMailer.Core.config.default.json";
public static string ConfigDirectory =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "BulkArchiveMailer");
public static string ConfigFilePath => Path.Combine(ConfigDirectory, "config.json");
public static string ReadEmbeddedDefaultJson()
{
var asm = Assembly.GetExecutingAssembly();
using (var stream = asm.GetManifestResourceStream(EmbeddedDefaultResourceName))
{
if (stream == null)
{
throw new InvalidOperationException(
$"Embedded resource '{EmbeddedDefaultResourceName}' not found in {asm.FullName}. " +
"Check that config.default.json's Build Action is set to Embedded Resource in BulkArchiveMailer.Core.csproj.");
}
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return reader.ReadToEnd();
}
}
}
public static AppSettingsData LoadDefaults() => Deserialize(ReadEmbeddedDefaultJson());
///
/// Loads settings from disk, seeding the file from the embedded
/// defaults on first run. A malformed hand-edited config.json is
/// never silently overwritten - the DataContractJsonSerializer
/// exception propagates so the caller can warn the operator
/// instead of quietly discarding their edits.
///
public static AppSettingsData Load()
{
if (!File.Exists(ConfigFilePath))
{
Directory.CreateDirectory(ConfigDirectory);
File.WriteAllText(ConfigFilePath, ReadEmbeddedDefaultJson(), new UTF8Encoding(false));
}
var json = File.ReadAllText(ConfigFilePath, Encoding.UTF8);
return Deserialize(json);
}
public static void Save(AppSettingsData data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
Directory.CreateDirectory(ConfigDirectory);
File.WriteAllText(ConfigFilePath, Serialize(data), new UTF8Encoding(false));
}
private static AppSettingsData Deserialize(string json)
{
var bytes = Encoding.UTF8.GetBytes(json);
using (var ms = new MemoryStream(bytes))
{
var serializer = new DataContractJsonSerializer(typeof(AppSettingsData));
return (AppSettingsData)serializer.ReadObject(ms);
}
}
private static string Serialize(AppSettingsData data)
{
using (var ms = new MemoryStream())
{
var serializer = new DataContractJsonSerializer(typeof(AppSettingsData));
serializer.WriteObject(ms, data);
var minified = Encoding.UTF8.GetString(ms.ToArray());
return PrettyPrintJson(minified);
}
}
///
/// DataContractJsonSerializer has no built-in indentation option
/// and writes single-line JSON. This tiny formatter re-indents it
/// so config.json stays comfortable to hand-edit (it is, after
/// all, the officially supported way to change the mail
/// templates). It only tracks string-literal boundaries, which is
/// sufficient for the flat/shallow shape this file always has.
///
private static string PrettyPrintJson(string json)
{
var sb = new StringBuilder(json.Length * 2);
var indent = 0;
var inString = false;
var escapeNext = false;
for (var i = 0; i < json.Length; i++)
{
var c = json[i];
if (inString)
{
sb.Append(c);
if (escapeNext)
{
escapeNext = false;
}
else if (c == '\\')
{
escapeNext = true;
}
else if (c == '"')
{
inString = false;
}
continue;
}
switch (c)
{
case '"':
inString = true;
sb.Append(c);
break;
case '{':
case '[':
sb.Append(c);
if (i + 1 < json.Length && (json[i + 1] == '}' || json[i + 1] == ']'))
{
sb.Append(json[i + 1]);
i++;
}
else
{
indent++;
sb.Append('\n').Append(' ', indent * 2);
}
break;
case '}':
case ']':
indent--;
sb.Append('\n').Append(' ', indent * 2).Append(c);
break;
case ',':
sb.Append(c).Append('\n').Append(' ', indent * 2);
break;
case ':':
sb.Append(c).Append(' ');
break;
default:
sb.Append(c);
break;
}
}
return sb.ToString();
}
}
}