261 lines
9.7 KiB
C#
261 lines
9.7 KiB
C#
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();
|
|
}
|
|
|
|
/// <summary>Shape of %APPDATA%\BulkArchiveMailer\config.json (and of the embedded config.default.json that seeds it).</summary>
|
|
[DataContract]
|
|
public sealed class AppSettingsData
|
|
{
|
|
[DataMember(Name = "archiveFolder")]
|
|
public string ArchiveFolder { get; set; } = "";
|
|
|
|
[DataMember(Name = "emailDomain")]
|
|
public string EmailDomain { get; set; } = "redlab.lo";
|
|
|
|
/// <summary>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.</summary>
|
|
[DataMember(Name = "testModeDefault")]
|
|
public bool TestModeDefault { get; set; } = true;
|
|
|
|
/// <summary>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.</summary>
|
|
[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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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());
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|