Compare commits

..

12 Commits

Author SHA1 Message Date
duffyduck 4484f19d14 Release v0.0.0.21 2026-04-03 19:32:20 +02:00
duffyduck ca17e5d433 Set Outlook security registry keys for all Office versions
Apply to both 16.0 (2016-2024/365) and 15.0 (2013) registry
paths. Costs nothing and ensures it works regardless of which
Office version is installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:31:08 +02:00
duffyduck d89e36b962 Widen main window, add all Outlook security registry keys, add hint
- Main window wider (830px) so all buttons fit without resizing
- Set ALL Outlook Object Model Guard registry values (not just 3)
- Clean removal: delete entire Security subkey when disabling
- Add hint in settings that Outlook restart is needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:29:54 +02:00
duffyduck 3092150d3a Release v0.0.0.20 2026-04-03 19:23:38 +02:00
duffyduck 163dc17b49 Add option to suppress Outlook security prompt
New setting "Outlook-Sicherheitsabfrage automatisch erlauben"
sets registry keys under HKCU\Policies\Microsoft\Office\16.0\
Outlook\Security to auto-approve Object Model Guard prompts.

Applied at app startup and when saving settings. Disabling the
option removes the registry values (back to Outlook default).
Works with all Outlook versions (2016-2024, same registry path).
No admin rights needed (HKCU).

Outlook must be restarted after changing this setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:22:53 +02:00
duffyduck b7cc335184 Add tray About menu, sync-on-start, sync lock
- "Ueber" menu item in tray context menu opens About dialog
- New user setting "Beim Start automatisch synchronisieren"
  syncs all enabled profiles once at app startup
- Sync lock prevents concurrent sync runs (timer, manual,
  on-start cannot overlap - second request is skipped)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:18:42 +02:00
duffyduck a19a39b7d2 Release v0.0.0.19 2026-04-03 18:56:01 +02:00
duffyduck 6349424007 Add debug logging and tag to UpdateContactAsync
- Include tags in PUT request (Starface may require it)
- Log failed updates with status code and response body
- Also committed: SafeGet for Outlook COM property reading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:54:52 +02:00
duffyduck 7ffaddc77f Use reflection-based SafeGet for Outlook COM property reading
Dynamic COM with ?? operator can fail silently for properties
that return COM null vs .NET null. Use GetType().InvokeMember()
which reliably reads any Outlook property and catches COM errors
per field instead of crashing the whole contact read.

Fixes Fax and other fields that may not have been read correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:54:10 +02:00
duffyduck 9007e2a9b5 Include Fax, HomePhone in matching and compatibility checks
- Fax and HomePhone are now checked for compatibility (filled on
  one side, empty on other = different contacts)
- Fax number can serve as a strong match together with Company
- Prevents fax-only contacts from being missed or mismatched

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:52:41 +02:00
duffyduck 0eadb2274e Release v0.0.0.18 2026-04-03 18:37:00 +02:00
duffyduck a676e064c4 Stricter contact matching to prevent false duplicates
New matching logic checks ALL identifying fields for compatibility:
- Empty vs filled field = different contacts (not a match)
- Both empty = compatible (ignored for matching)
- Both filled = must be equal

Fields checked: Email, FirstName, LastName, Company, PhoneWork,
PhoneMobile. Requires at least one strong match (email, name,
or phone) plus no conflicting fields.

Example: Two "Max Mustermann" where one has Company="Firma A"
and the other has empty Company are now correctly identified as
different contacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:35:44 +02:00
9 changed files with 239 additions and 79 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.17"
#define MyAppVersion "0.0.0.21"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"
@@ -1,5 +1,6 @@
using System;
using System.IO;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace StarfaceOutlookSync.Models
@@ -7,6 +8,8 @@ namespace StarfaceOutlookSync.Models
public class UserSettings
{
public bool StartMinimized { get; set; } = false;
public bool SyncOnStart { get; set; } = false;
public bool AutoAcceptOutlookPrompt { get; set; } = false;
private static readonly string SettingsFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@@ -32,6 +35,48 @@ namespace StarfaceOutlookSync.Models
File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented));
}
catch { }
ApplyOutlookSecuritySetting();
}
public void ApplyOutlookSecuritySetting()
{
// Alle Office-Versionen abdecken (16.0 = 2016/2019/2021/2024/365, 15.0 = 2013)
var versions = new[] { "16.0", "15.0" };
foreach (var ver in versions)
{
try
{
var regPath = $@"Software\Policies\Microsoft\Office\{ver}\Outlook\Security";
if (AutoAcceptOutlookPrompt)
{
var key = Registry.CurrentUser.CreateSubKey(regPath);
key.SetValue("ObjectModelGuard", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMAddressBookAccess", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMAddressInformationAccess", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMSend", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMSaveAs", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMFormulaAccess", 2, RegistryValueKind.DWord);
key.SetValue("PromptOOMCustomAction", 2, RegistryValueKind.DWord);
key.SetValue("PromptSimpleMAPISend", 2, RegistryValueKind.DWord);
key.SetValue("PromptSimpleMAPINameResolve", 2, RegistryValueKind.DWord);
key.SetValue("PromptSimpleMAPIOpenMessage", 2, RegistryValueKind.DWord);
key.SetValue("AdminSecurityMode", 3, RegistryValueKind.DWord);
key.Close();
}
else
{
try
{
Registry.CurrentUser.DeleteSubKey(regPath, false);
}
catch { }
}
}
catch { }
}
}
}
}
@@ -394,29 +394,40 @@ namespace StarfaceOutlookSync.Services
}
}
private static string SafeGet(dynamic ci, string property)
{
try
{
object val = ci.GetType().InvokeMember(property,
System.Reflection.BindingFlags.GetProperty, null, ci, null);
return val?.ToString() ?? "";
}
catch { return ""; }
}
private UnifiedContact MapFromOutlook(dynamic ci)
{
return new UnifiedContact
{
OutlookEntryId = (string)(ci.EntryID ?? ""),
FirstName = (string)(ci.FirstName ?? ""),
LastName = (string)(ci.LastName ?? ""),
Company = (string)(ci.CompanyName ?? ""),
JobTitle = (string)(ci.JobTitle ?? ""),
Email = (string)(ci.Email1Address ?? ""),
EmailSecondary = (string)(ci.Email2Address ?? ""),
PhoneWork = (string)(ci.BusinessTelephoneNumber ?? ""),
PhoneMobile = (string)(ci.MobileTelephoneNumber ?? ""),
PhoneHome = (string)(ci.HomeTelephoneNumber ?? ""),
Fax = (string)(ci.BusinessFaxNumber ?? ""),
Street = (string)(ci.BusinessAddressStreet ?? ""),
City = (string)(ci.BusinessAddressCity ?? ""),
PostalCode = (string)(ci.BusinessAddressPostalCode ?? ""),
State = (string)(ci.BusinessAddressState ?? ""),
Country = (string)(ci.BusinessAddressCountry ?? ""),
Website = (string)(ci.WebPage ?? ""),
Notes = (string)(ci.Body ?? ""),
Salutation = (string)(ci.Title ?? ""),
OutlookEntryId = SafeGet(ci, "EntryID"),
FirstName = SafeGet(ci, "FirstName"),
LastName = SafeGet(ci, "LastName"),
Company = SafeGet(ci, "CompanyName"),
JobTitle = SafeGet(ci, "JobTitle"),
Email = SafeGet(ci, "Email1Address"),
EmailSecondary = SafeGet(ci, "Email2Address"),
PhoneWork = SafeGet(ci, "BusinessTelephoneNumber"),
PhoneMobile = SafeGet(ci, "MobileTelephoneNumber"),
PhoneHome = SafeGet(ci, "HomeTelephoneNumber"),
Fax = SafeGet(ci, "BusinessFaxNumber"),
Street = SafeGet(ci, "BusinessAddressStreet"),
City = SafeGet(ci, "BusinessAddressCity"),
PostalCode = SafeGet(ci, "BusinessAddressPostalCode"),
State = SafeGet(ci, "BusinessAddressState"),
Country = SafeGet(ci, "BusinessAddressCountry"),
Website = SafeGet(ci, "WebPage"),
Notes = SafeGet(ci, "Body"),
Salutation = SafeGet(ci, "Title"),
Birthday = GetBirthdayString(ci)
};
}
@@ -425,8 +436,9 @@ namespace StarfaceOutlookSync.Services
{
try
{
DateTime bday = ci.Birthday;
if (bday.Year > 1900 && bday != DateTime.MinValue)
object val = ci.GetType().InvokeMember("Birthday",
System.Reflection.BindingFlags.GetProperty, null, ci, null);
if (val is DateTime bday && bday.Year > 1900 && bday != DateTime.MinValue)
return bday.ToString("yyyy-MM-dd");
}
catch { }
@@ -304,12 +304,27 @@ namespace StarfaceOutlookSync.Services
{
var sfContact = MapToStarface(contact);
sfContact["id"] = contactId;
// Tag beibehalten
if (!string.IsNullOrEmpty(book.TagId))
{
sfContact["tags"] = new JArray { new JObject { ["id"] = book.TagId } };
}
var query = "";
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}";
var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json");
var body = sfContact.ToString();
var content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _http.PutAsync($"{_baseUrl}/contacts/{contactId}{query}", content);
if (!resp.IsSuccessStatusCode)
{
var respBody = await resp.Content.ReadAsStringAsync();
OnDebug?.Invoke($"PUT /contacts/{contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
}
return resp.IsSuccessStatusCode;
}
+77 -44
View File
@@ -17,61 +17,94 @@ namespace StarfaceOutlookSync.Services
/// <summary>
/// Findet einen passenden Kontakt in der Kandidatenliste.
/// Matching-Reihenfolge: E-Mail, dann Vorname+Nachname+Firma, dann Vorname+Nachname.
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
/// auf der anderen auch gefuellt (und gleich) sein.
/// Ein leeres Feld auf einer Seite und ein gefuelltes auf der anderen
/// bedeutet: verschiedene Kontakte.
/// </summary>
private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
{
if (candidates == null || candidates.Count == 0) return null;
// 1. Exakte E-Mail
if (!string.IsNullOrEmpty(contact.Email))
foreach (var c in candidates)
{
var byEmail = candidates.FirstOrDefault(c =>
!string.IsNullOrEmpty(c.Email) &&
c.Email.Equals(contact.Email, StringComparison.OrdinalIgnoreCase));
if (byEmail != null) return byEmail;
}
// 2. Vorname + Nachname + Firma (staerkstes Match ohne E-Mail)
if ((!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName))
&& !string.IsNullOrEmpty(contact.Company))
{
var byNameCompany = candidates.FirstOrDefault(c =>
c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) &&
c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) &&
c.Company.Equals(contact.Company, StringComparison.OrdinalIgnoreCase));
if (byNameCompany != null) return byNameCompany;
}
// 3. Vorname + Nachname (ohne Firma)
if (!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName))
{
var byName = candidates.FirstOrDefault(c =>
c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) &&
c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) &&
(!string.IsNullOrEmpty(c.FirstName) || !string.IsNullOrEmpty(c.LastName)));
if (byName != null) return byName;
}
// 4. Telefonnummer (Buero oder Mobil)
if (!string.IsNullOrEmpty(contact.PhoneWork))
{
var byPhone = candidates.FirstOrDefault(c =>
!string.IsNullOrEmpty(c.PhoneWork) &&
NormalizePhone(c.PhoneWork) == NormalizePhone(contact.PhoneWork));
if (byPhone != null) return byPhone;
}
if (!string.IsNullOrEmpty(contact.PhoneMobile))
{
var byMobile = candidates.FirstOrDefault(c =>
!string.IsNullOrEmpty(c.PhoneMobile) &&
NormalizePhone(c.PhoneMobile) == NormalizePhone(contact.PhoneMobile));
if (byMobile != null) return byMobile;
if (IsMatch(contact, c))
return c;
}
return null;
}
private static bool IsMatch(UnifiedContact a, UnifiedContact b)
{
// Mindestens ein identifizierendes Feld muss vorhanden sein
bool hasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
bool hasEmail = !string.IsNullOrEmpty(a.Email);
bool hasPhone = !string.IsNullOrEmpty(a.PhoneWork) || !string.IsNullOrEmpty(a.PhoneMobile);
if (!hasName && !hasEmail && !hasPhone) return false;
// E-Mail: wenn auf beiden Seiten vorhanden, muss sie gleich sein
// Wenn nur auf einer Seite vorhanden -> kein Match
if (!FieldsCompatible(a.Email, b.Email)) return false;
// Name: wenn auf einer Seite vorhanden, muss er gleich sein
if (!FieldsCompatible(a.FirstName, b.FirstName)) return false;
if (!FieldsCompatible(a.LastName, b.LastName)) return false;
// Firma: wenn auf einer Seite vorhanden, muss sie gleich sein
// Leere Firma vs. gefuellte Firma = verschiedene Kontakte
if (!FieldsCompatible(a.Company, b.Company)) return false;
// Telefon/Fax: wenn auf einer Seite vorhanden, muss es gleich sein
if (!PhoneFieldsCompatible(a.PhoneWork, b.PhoneWork)) return false;
if (!PhoneFieldsCompatible(a.PhoneMobile, b.PhoneMobile)) return false;
if (!PhoneFieldsCompatible(a.PhoneHome, b.PhoneHome)) return false;
if (!PhoneFieldsCompatible(a.Fax, b.Fax)) return false;
// Mindestens ein starkes Match muss vorhanden sein
bool emailMatch = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email)
&& a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase);
bool nameMatch = hasName
&& a.FirstName.Equals(b.FirstName, StringComparison.OrdinalIgnoreCase)
&& a.LastName.Equals(b.LastName, StringComparison.OrdinalIgnoreCase)
&& (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName));
bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork)
&& NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork))
|| (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax)
&& NormalizePhone(a.Fax) == NormalizePhone(b.Fax));
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
// Email oder Name reicht. Telefon/Fax nur mit Firma zusammen.
return emailMatch || nameMatch || (phoneMatch && companyMatch) || (companyMatch && phoneMatch);
}
/// <summary>
/// Prueft ob zwei Felder kompatibel sind.
/// Beide leer = kompatibel. Beide gleich = kompatibel.
/// Eins leer, eins gefuellt = NICHT kompatibel (verschiedene Kontakte).
/// </summary>
private static bool FieldsCompatible(string a, string b)
{
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return true;
if (aEmpty != bEmpty) return false; // Einer leer, anderer nicht
return a.Equals(b, StringComparison.OrdinalIgnoreCase);
}
private static bool PhoneFieldsCompatible(string a, string b)
{
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return true;
if (aEmpty != bEmpty) return false;
return NormalizePhone(a) == NormalizePhone(b);
}
private static string NormalizePhone(string phone)
{
if (string.IsNullOrEmpty(phone)) return "";
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.0.17</Version>
<AssemblyVersion>0.0.0.17</AssemblyVersion>
<FileVersion>0.0.0.17</FileVersion>
<Version>0.0.0.21</Version>
<AssemblyVersion>0.0.0.21</AssemblyVersion>
<FileVersion>0.0.0.21</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label
{
Text = "Version 0.0.0.17",
Text = "Version 0.0.0.21",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray
+33 -3
View File
@@ -22,6 +22,7 @@ namespace StarfaceOutlookSync.UI
private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer;
private volatile bool _syncRunning = false;
public MainForm()
{
@@ -30,14 +31,31 @@ namespace StarfaceOutlookSync.UI
SetupAutoSync();
RefreshProfileList();
// Minimiert starten falls in Einstellungen aktiviert
// Einstellungen laden und anwenden
var settings = UserSettings.Load();
settings.ApplyOutlookSecuritySetting();
if (settings.StartMinimized)
{
WindowState = FormWindowState.Minimized;
ShowInTaskbar = false;
Visible = false;
}
// Beim Start automatisch synchronisieren
if (settings.SyncOnStart)
{
_ = SyncAllProfiles();
}
}
private async Task SyncAllProfiles()
{
var profiles = _profileManager.GetProfiles().Where(p => p.Enabled).ToList();
foreach (var profile in profiles)
{
await RunSync(profile);
}
}
protected override void SetVisibleCore(bool value)
@@ -58,8 +76,8 @@ namespace StarfaceOutlookSync.UI
private void InitializeComponent()
{
Text = "Starface Kontakt-Sync";
Size = new Size(620, 450);
MinimumSize = new Size(500, 350);
Size = new Size(830, 450);
MinimumSize = new Size(830, 350);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9);
Icon = AppIcon.GetIcon();
@@ -158,6 +176,7 @@ namespace StarfaceOutlookSync.UI
if (profiles.Any(p => p.Enabled))
_trayMenu.Items.Add("-");
_trayMenu.Items.Add("Ueber", null, (s, e) => ShowAbout());
_trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication());
}
@@ -318,6 +337,13 @@ namespace StarfaceOutlookSync.UI
private async Task RunSync(SyncProfile profile)
{
if (_syncRunning)
{
SetStatus("Sync laeuft bereits, bitte warten...");
return;
}
_syncRunning = true;
try
{
SetStatus($"Synchronisiere '{profile.Name}'...");
@@ -340,6 +366,10 @@ namespace StarfaceOutlookSync.UI
ex.Message, ToolTipIcon.Error);
SetStatus($"Fehler: {ex.Message}");
}
finally
{
_syncRunning = false;
}
}
private void SetStatus(string text)
+30 -5
View File
@@ -6,7 +6,7 @@ namespace StarfaceOutlookSync.UI
{
public class SettingsForm : Form
{
private CheckBox _chkStartMinimized;
private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook;
private Button _btnSave, _btnCancel;
private readonly UserSettings _settings;
@@ -19,7 +19,7 @@ namespace StarfaceOutlookSync.UI
private void InitializeComponent()
{
Text = "Einstellungen";
Size = new Size(350, 180);
Size = new Size(380, 250);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
@@ -33,20 +33,43 @@ namespace StarfaceOutlookSync.UI
Checked = _settings.StartMinimized
};
_chkSyncOnStart = new CheckBox
{
Text = "Beim Start automatisch synchronisieren",
Left = 20, Top = 52, AutoSize = true,
Checked = _settings.SyncOnStart
};
_chkAutoAcceptOutlook = new CheckBox
{
Text = "Outlook-Sicherheitsabfrage automatisch erlauben",
Left = 20, Top = 80, AutoSize = true,
Checked = _settings.AutoAcceptOutlookPrompt
};
var lblHint = new Label
{
Text = "Hinweis: Outlook muss nach Aenderung dieser Option\nneu gestartet werden.",
Left = 38, Top = 102, Width = 300, Height = 32,
ForeColor = Color.Gray,
Font = new Font("Segoe UI", 8)
};
_btnSave = new Button
{
Text = "Speichern", Left = 80, Top = 100, Width = 85, Height = 28,
Text = "Speichern", Left = 95, Top = 170, Width = 85, Height = 28,
DialogResult = DialogResult.None
};
_btnSave.Click += (s, e) => Save();
_btnCancel = new Button
{
Text = "Abbrechen", Left = 174, Top = 100, Width = 85, Height = 28,
Text = "Abbrechen", Left = 189, Top = 170, Width = 85, Height = 28,
DialogResult = DialogResult.Cancel
};
Controls.AddRange(new Control[] { _chkStartMinimized, _btnSave, _btnCancel });
Size = new Size(380, 260);
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint, _btnSave, _btnCancel });
AcceptButton = _btnSave;
CancelButton = _btnCancel;
}
@@ -54,6 +77,8 @@ namespace StarfaceOutlookSync.UI
private void Save()
{
_settings.StartMinimized = _chkStartMinimized.Checked;
_settings.SyncOnStart = _chkSyncOnStart.Checked;
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
_settings.Save();
DialogResult = DialogResult.OK;
Close();