Compare commits

..

14 Commits

Author SHA1 Message Date
duffyduck ad323c9d0f Release v0.0.0.24 2026-06-08 09:58:26 +02:00
duffyduck 849a996b9a Fix sync duplicates and extreme slowness
Behebt Dubletten auf beiden Seiten und sehr langsame Syncs:

- Getrennte Hash-Baselines pro Seite (LastOutlookHash/LastStarfaceHash)
  statt eines gemeinsamen Hashes. Outlook und Starface stellen denselben
  Kontakt unterschiedlich dar, wodurch der gemeinsame Hash nie passte und
  bei jedem Lauf praktisch jeder Kontakt neu geschrieben wurde.
- Update-Methoden geben den frisch eingelesenen Stand zurueck, damit die
  Baseline nach dem Schreiben korrekt gesetzt wird (sauberes Konvergieren).
- Unvollstaendig geladene Starface-Liste bricht jetzt mit Fehler ab
  (inkl. Retry) statt still mit Teil-Liste weiterzuarbeiten - das liess
  Kontakte faelschlich als geloescht erscheinen und erzeugte Dubletten.
- Fehlender Starface-Kontakt (anderes Adressbuch) behaelt das Mapping,
  statt es zu verwerfen und neu anzulegen.
- Lockereres Re-Matching: gleicher E-Mail- oder voller Namens-Treffer
  reicht; umformatierte Telefonnummern blockieren ihn nicht mehr.
- Starface-Kontaktdetails werden parallel geladen (8 gleichzeitig).

Bestehende Mappings werden beim ersten Sync automatisch migriert.
CHANGELOG.md hinzugefuegt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:57:37 +02:00
duffyduck b07a3b3a87 Fix critical delete bug: don't mass-delete on EntryID change
Two fixes for delete propagation in Phase 1:

1. When Outlook contact not found by EntryID, try to re-match
   by name/email/phone before assuming it was deleted. Outlook
   can change EntryIDs on restart or profile changes, causing
   the sync to think ALL contacts were deleted.

2. When Starface contact not found in current list, DON'T delete
   from Outlook. The contact may belong to a different address
   book. Just drop the mapping and let Phase 2/3 re-link it.

These changes make delete propagation much safer and prevent
accidental mass-deletion of contacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:03:51 +02:00
duffyduck f620e96d23 Document Outlook security prompt suppression in README
Include step-by-step instructions for both standalone PCs and
domain/Terminal Server environments where GPO locks the setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:55:41 +02:00
duffyduck e8731a87d4 Release v0.0.0.23 2026-04-03 19:50:03 +02:00
duffyduck 53ca4611d1 Write Outlook security keys to HKLM for domain environments
On domain PCs, HKCU policies are controlled by GPO and the
Trust Center settings are greyed out. Now also writes to HKLM
(requires admin rights) which overrides GPO settings.

Shows orange hint in settings when GPO lock is detected:
"Auf Domaenen-PCs: App einmalig als Admin starten!"

The app tries all 8 combinations: HKCU/HKLM x Policies/direct
x 16.0/15.0. Silently skips paths where permissions are denied.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:48:09 +02:00
duffyduck df13ddf6b1 Release v0.0.0.22 2026-04-03 19:38:02 +02:00
duffyduck c08a625348 Write Outlook security keys to both Policies and normal user path
On Terminal Servers, normal users cannot write to HKCU\Software\
Policies. Now also writes to HKCU\Software\Microsoft\Office\...\
Security which is always writable and also read by Outlook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:37:26 +02:00
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
12 changed files with 462 additions and 126 deletions
+65
View File
@@ -0,0 +1,65 @@
# Changelog
Alle nennenswerten Aenderungen an Starface Outlook Sync werden hier dokumentiert.
Format orientiert sich an [Keep a Changelog](https://keepachangelog.com/de/),
Versionsschema ist `x.x.x.x` (siehe `release.sh`).
## [Unreleased]
### Behoben
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
Ursachen wurden beseitigt:
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
Lade-Fehler oder Timeout) liess die Engine Kontakte faelschlich als
"geloescht" ansehen, ihr Mapping verwerfen und sie beim naechsten Lauf neu
anlegen. Der Kontakt-Abruf bricht jetzt mit Fehlermeldung ab (inkl.
Wiederholversuch), statt still mit einer Teil-Liste weiterzuarbeiten.
- Ist ein Starface-Kontakt nicht in der geladenen Liste (z.B. anderes
Adressbuch), wird das Mapping jetzt **behalten** statt verworfen und neu
angelegt.
- Das Wiederzuordnen bestehender Kontakte war zu streng: eine von Starface
umformatierte Telefonnummer konnte einen eindeutigen E-Mail- oder
Namens-Treffer ueberstimmen und so eine Neuanlage statt Verknuepfung
ausloesen. Ein gleicher E-Mail- oder voller Namens-Treffer reicht jetzt.
- **Synchronisation extrem langsam / schrieb bei jedem Lauf alle Kontakte neu.**
Die Aenderungserkennung verglich einen einzigen Hash gegen beide Seiten.
Outlook und Starface stellen denselben Kontakt aber unterschiedlich dar
(nicht uebertragene Felder, abweichendes Telefonformat), wodurch der Hash nie
uebereinstimmte und praktisch jeder Kontakt bei jedem Sync neu geschrieben
wurde. Jede Seite hat jetzt eine eigene Baseline (`LastOutlookHash` /
`LastStarfaceHash`); nur tatsaechlich geaenderte Kontakte werden geschrieben.
### Geaendert
- Starface-Kontaktdetails werden beim Laden parallel abgerufen (8 gleichzeitig)
statt einzeln nacheinander deutlich schneller bei grossen Adressbuechern.
- `UpdateContact` (Outlook) und `UpdateContactAsync` (Starface) geben jetzt den
frisch eingelesenen Stand zurueck, damit die Baseline nach dem Schreiben
korrekt gesetzt wird und der Sync sauber konvergiert.
### Migration
- Bestehende Mapping-Dateien werden beim ersten Sync automatisch uebernommen
(ein ruhiger Durchlauf ohne Massen-Update).
- **Bereits vorhandene Dubletten werden nicht automatisch entfernt** die Fixes
verhindern nur neue. Vorhandene Doppel-Kontakte einmalig manuell bereinigen.
## [0.0.0.23]
### Behoben
- Kritischer Loesch-Fehler: Kontakte wurden bei einer geaenderten Outlook-EntryID
nicht mehr massenhaft geloescht.
### Geaendert
- Outlook-Sicherheitsschluessel werden fuer Domaenenumgebungen zusaetzlich nach
HKLM geschrieben (unterdrueckt die Outlook-Sicherheitsabfrage).
## Aeltere Versionen
Aeltere Releases (v0.0.0.1 v0.0.0.22) sind ueber die Git-Historie und die
Git-Tags nachvollziehbar.
+26
View File
@@ -42,6 +42,32 @@ Windows-Anwendung zur bidirektionalen Synchronisation von Kontakten zwischen Mic
5. Starface-Adressbuch und Outlook-Kontaktordner waehlen 5. Starface-Adressbuch und Outlook-Kontaktordner waehlen
6. Speichern und "Jetzt synchronisieren" 6. Speichern und "Jetzt synchronisieren"
### Outlook-Sicherheitsabfrage unterdruecken
Beim Zugriff auf Outlook-Kontakte zeigt Outlook standardmaessig einen
Sicherheitsdialog ("Ein Programm versucht auf Ihre E-Mail-Adressinformationen
zuzugreifen"). Dieser kann in den Einstellungen der App deaktiviert werden:
1. In der App auf "Einstellungen" klicken
2. "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern
**Auf Domaenen-PCs / Terminal Servern:**
Die Outlook-Sicherheitseinstellungen werden dort per Gruppenrichtlinie (GPO)
gesteuert und sind im Trust Center ausgegraut. In diesem Fall muss die App
**einmalig als Administrator** gestartet werden, damit die Registry-Keys
unter HKLM geschrieben werden koennen:
1. Rechtsklick auf die App -> "Als Administrator ausfuehren"
2. Einstellungen -> "Outlook-Sicherheitsabfrage automatisch erlauben" aktivieren
3. Speichern und App schliessen
4. Outlook neu starten
5. App kann danach wieder normal (ohne Admin) gestartet werden
Die Einstellung bleibt dauerhaft bestehen und gilt fuer alle Benutzer
auf dem Rechner.
### Deinstallation ### Deinstallation
Ueber Windows Einstellungen -> Apps oder die Systemsteuerung. Ueber Windows Einstellungen -> Apps oder die Systemsteuerung.
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php) ; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync" #define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.19" #define MyAppVersion "0.0.0.24"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation" #define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de" #define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe" #define MyAppExeName "StarfaceOutlookSync.exe"
@@ -45,6 +45,15 @@ namespace StarfaceOutlookSync.Models
public string ProfileId { get; set; } = ""; public string ProfileId { get; set; } = "";
public string OutlookEntryId { get; set; } = ""; public string OutlookEntryId { get; set; } = "";
public string StarfaceId { get; set; } = ""; public string StarfaceId { get; set; } = "";
// Getrennte Baselines pro Seite. Outlook und Starface stellen denselben
// Kontakt unterschiedlich dar (Felder, Telefonformat), daher MUSS jede
// Seite gegen ihre eigene zuletzt-gesehene Repraesentation verglichen
// werden - sonst gilt jeder Kontakt bei jedem Sync als geaendert.
public string LastOutlookHash { get; set; } = "";
public string LastStarfaceHash { get; set; } = "";
// Alt-Feld (vor v0.0.0.24). Nur noch fuer Migration bestehender Mappings.
public string LastSyncHash { get; set; } = ""; public string LastSyncHash { get; set; } = "";
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using Microsoft.Win32;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace StarfaceOutlookSync.Models namespace StarfaceOutlookSync.Models
@@ -7,6 +8,8 @@ namespace StarfaceOutlookSync.Models
public class UserSettings public class UserSettings
{ {
public bool StartMinimized { get; set; } = false; 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( private static readonly string SettingsFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@@ -32,6 +35,86 @@ namespace StarfaceOutlookSync.Models
File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented)); File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented));
} }
catch { } catch { }
ApplyOutlookSecuritySetting();
}
public void ApplyOutlookSecuritySetting()
{
var versions = new[] { "16.0", "15.0" };
var securityValues = new (string name, int value)[]
{
("ObjectModelGuard", 2),
("PromptOOMAddressBookAccess", 2),
("PromptOOMAddressInformationAccess", 2),
("PromptOOMSend", 2),
("PromptOOMSaveAs", 2),
("PromptOOMFormulaAccess", 2),
("PromptOOMCustomAction", 2),
("PromptSimpleMAPISend", 2),
("PromptSimpleMAPINameResolve", 2),
("PromptSimpleMAPIOpenMessage", 2),
("AdminSecurityMode", 3),
};
// In alle moeglichen Pfade schreiben (HKCU + HKLM, Policies + direkt)
var roots = new[] { Registry.CurrentUser, Registry.LocalMachine };
var prefixes = new[]
{
@"Software\Policies\Microsoft\Office",
@"Software\Microsoft\Office"
};
foreach (var ver in versions)
{
foreach (var root in roots)
{
foreach (var prefix in prefixes)
{
var regPath = $@"{prefix}\{ver}\Outlook\Security";
try
{
if (AutoAcceptOutlookPrompt)
{
var key = root.CreateSubKey(regPath);
if (key != null)
{
foreach (var (name, value) in securityValues)
key.SetValue(name, value, RegistryValueKind.DWord);
key.Close();
}
}
else
{
try { root.DeleteSubKey(regPath, false); } catch { }
}
}
catch { } // Kein Fehler wenn Rechte fehlen - naechsten Pfad versuchen
}
}
}
}
/// <summary>
/// Prueft ob die Outlook-Sicherheitseinstellung per GPO blockiert wird.
/// </summary>
public static bool IsOutlookSecurityLockedByPolicy()
{
try
{
// Wenn HKLM Policies gesetzt sind und wir dort nicht schreiben koennen
var key = Registry.LocalMachine.OpenSubKey(
@"Software\Policies\Microsoft\Office\16.0\Outlook\Security", false);
if (key != null)
{
var val = key.GetValue("AdminSecurityMode");
key.Close();
if (val != null) return true;
}
}
catch { }
return false;
} }
} }
} }
@@ -350,7 +350,13 @@ namespace StarfaceOutlookSync.Services
} }
} }
public bool UpdateContact(string entryId, UnifiedContact contact) /// <summary>
/// Aktualisiert den Outlook-Kontakt und gibt den frisch eingelesenen
/// Stand zurueck (null bei Fehler). Der zurueckgegebene Kontakt liefert
/// den massgeblichen Hash NACH dem Schreiben - noetig damit die naechste
/// Synchronisation den Kontakt nicht erneut als geaendert erkennt.
/// </summary>
public UnifiedContact UpdateContact(string entryId, UnifiedContact contact)
{ {
try try
{ {
@@ -361,15 +367,17 @@ namespace StarfaceOutlookSync.Services
MapToOutlook(contact, ci); MapToOutlook(contact, ci);
ci.Save(); ci.Save();
var updated = MapFromOutlook(ci);
Marshal.ReleaseComObject(ci); Marshal.ReleaseComObject(ci);
Marshal.ReleaseComObject(ns); Marshal.ReleaseComObject(ns);
return true; return updated;
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Error updating contact: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error updating contact: {ex.Message}");
return false; return null;
} }
} }
@@ -194,7 +194,16 @@ namespace StarfaceOutlookSync.Services
query += $"&tags={book.TagId}"; query += $"&tags={book.TagId}";
var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}"); var resp = await _http.GetAsync($"{_baseUrl}/contacts?{query}");
if (!resp.IsSuccessStatusCode) break; if (!resp.IsSuccessStatusCode)
{
// WICHTIG: nicht still abbrechen. Eine unvollstaendige Liste
// laesst die Sync-Engine Kontakte faelschlich als geloescht
// ansehen -> Mappings werden verworfen -> Dubletten.
throw new Exception(
$"Starface-Kontaktliste konnte nicht vollstaendig geladen werden " +
$"(Seite {page}: HTTP {(int)resp.StatusCode}). Synchronisation abgebrochen, " +
$"um Dubletten zu vermeiden.");
}
var body = await resp.Content.ReadAsStringAsync(); var body = await resp.Content.ReadAsStringAsync();
JArray array; JArray array;
@@ -232,31 +241,40 @@ namespace StarfaceOutlookSync.Services
OnDebug?.Invoke($"Seite {page}: {array.Count} Kontakte in Liste"); OnDebug?.Invoke($"Seite {page}: {array.Count} Kontakte in Liste");
// Die Listen-API gibt nur Summary zurueck. if (firstPage)
// Jeden Kontakt einzeln abrufen fuer alle Felder.
foreach (var item in array)
{ {
var id = item["id"]?.ToString(); var firstId = array[0]?["id"]?.ToString();
if (string.IsNullOrEmpty(id)) continue; if (!string.IsNullOrEmpty(firstId))
try
{ {
var detailResp = await _http.GetAsync($"{_baseUrl}/contacts/{id}"); var sample = await FetchDetailAsync(firstId);
if (detailResp.IsSuccessStatusCode) if (sample != null)
{ OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{sample.ToString(Formatting.Indented)}");
var detailBody = await detailResp.Content.ReadAsStringAsync();
var detailObj = JObject.Parse(detailBody);
if (firstPage)
{
OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{detailObj.ToString(Formatting.Indented)}");
firstPage = false;
}
contacts.Add(MapFromStarface(detailObj));
}
} }
catch { } firstPage = false;
}
// Die Listen-API liefert nur eine Zusammenfassung; jeder Kontakt
// muss einzeln geladen werden. Das parallelisieren (begrenzt),
// sonst dauert es bei vielen Kontakten extrem lange.
var ids = array
.Select(it => it["id"]?.ToString())
.Where(id => !string.IsNullOrEmpty(id))
.ToList();
const int maxParallel = 8;
for (int i = 0; i < ids.Count; i += maxParallel)
{
var batch = ids.Skip(i).Take(maxParallel)
.Select(async id =>
{
var detail = await FetchDetailAsync(id);
// null = 404 (zwischenzeitlich geloescht) -> ueberspringen.
return detail == null ? null : MapFromStarface(detail);
})
.ToList();
var mapped = await Task.WhenAll(batch);
contacts.AddRange(mapped.Where(c => c != null));
} }
if (array.Count < pageSize) break; if (array.Count < pageSize) break;
@@ -266,6 +284,36 @@ namespace StarfaceOutlookSync.Services
return contacts; return contacts;
} }
/// <summary>
/// Laedt das Detail-JSON eines Kontakts mit kleiner Wiederholung.
/// Gibt null zurueck, wenn der Kontakt zwischen Listen- und Detail-Abruf
/// wirklich geloescht wurde (404 - harmlos, wird uebersprungen).
/// Wirft bei transienten Fehlern, damit der Aufrufer NICHT mit einer
/// unvollstaendigen Liste weiterarbeitet (sonst entstehen Dubletten).
/// </summary>
private async Task<JObject> FetchDetailAsync(string id)
{
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
var resp = await _http.GetAsync($"{_baseUrl}/contacts/{id}");
if (resp.IsSuccessStatusCode)
return JObject.Parse(await resp.Content.ReadAsStringAsync());
// 404 = zwischenzeitlich geloescht; nicht erneut versuchen.
if (resp.StatusCode == HttpStatusCode.NotFound) return null;
}
catch { }
await Task.Delay(250 * (attempt + 1));
}
throw new Exception(
$"Starface-Kontakt {id} konnte nach mehreren Versuchen nicht geladen werden. " +
$"Synchronisation abgebrochen, um Dubletten zu vermeiden.");
}
public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book) public async Task<UnifiedContact> CreateContactAsync(UnifiedContact contact, StarfaceAddressBook book)
{ {
var sfContact = MapToStarface(contact); var sfContact = MapToStarface(contact);
@@ -300,7 +348,13 @@ namespace StarfaceOutlookSync.Services
return MapFromStarface(created); return MapFromStarface(created);
} }
public async Task<bool> UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book) /// <summary>
/// Aktualisiert den Starface-Kontakt und gibt den massgeblichen Stand
/// NACH dem Schreiben zurueck (null bei Fehler). Wird fuer die getrennte
/// Hash-Baseline benoetigt, damit der Kontakt beim naechsten Sync nicht
/// erneut faelschlich als geaendert gilt.
/// </summary>
public async Task<UnifiedContact> UpdateContactAsync(string contactId, UnifiedContact contact, StarfaceAddressBook book)
{ {
var sfContact = MapToStarface(contact); var sfContact = MapToStarface(contact);
sfContact["id"] = contactId; sfContact["id"] = contactId;
@@ -323,9 +377,37 @@ namespace StarfaceOutlookSync.Services
{ {
var respBody = await resp.Content.ReadAsStringAsync(); var respBody = await resp.Content.ReadAsStringAsync();
OnDebug?.Invoke($"PUT /contacts/{contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}"); OnDebug?.Invoke($"PUT /contacts/{contactId} fehlgeschlagen: {(int)resp.StatusCode}\n{respBody}");
return null;
} }
return resp.IsSuccessStatusCode; // Frischen Stand zurueckgeben. Manche Versionen liefern den Kontakt
// direkt in der PUT-Antwort, sonst per GET nachladen.
try
{
var respBody = await resp.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(respBody))
{
var obj = JObject.Parse(respBody);
if (obj["blocks"] != null)
return MapFromStarface(obj);
}
}
catch { }
return await GetContactAsync(contactId) ?? contact;
}
/// <summary>Laedt einen einzelnen Kontakt mit allen Feldern.</summary>
public async Task<UnifiedContact> GetContactAsync(string contactId)
{
try
{
var resp = await _http.GetAsync($"{_baseUrl}/contacts/{contactId}");
if (!resp.IsSuccessStatusCode) return null;
var obj = JObject.Parse(await resp.Content.ReadAsStringAsync());
return MapFromStarface(obj);
}
catch { return null; }
} }
public async Task<bool> DeleteContactAsync(string contactId) public async Task<bool> DeleteContactAsync(string contactId)
+89 -85
View File
@@ -37,72 +37,38 @@ namespace StarfaceOutlookSync.Services
private static bool IsMatch(UnifiedContact a, UnifiedContact b) 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 hasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName); && (!string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName));
bool hasEmail = !string.IsNullOrEmpty(a.Email);
bool hasPhone = !string.IsNullOrEmpty(a.PhoneWork) || !string.IsNullOrEmpty(a.PhoneMobile);
if (!hasName && !hasEmail && !hasPhone) return false; // Starke Identifikatoren
// 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) bool emailMatch = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email)
&& a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase); && a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase);
bool nameMatch = hasName bool nameMatch = hasName
&& a.FirstName.Equals(b.FirstName, StringComparison.OrdinalIgnoreCase) && (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase)
&& a.LastName.Equals(b.LastName, 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) bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork)
&& NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork)) && NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork))
|| (!string.IsNullOrEmpty(a.PhoneMobile) && !string.IsNullOrEmpty(b.PhoneMobile)
&& NormalizePhone(a.PhoneMobile) == NormalizePhone(b.PhoneMobile))
|| (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax) || (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax)
&& NormalizePhone(a.Fax) == NormalizePhone(b.Fax)); && NormalizePhone(a.Fax) == NormalizePhone(b.Fax));
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company) bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase); && a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
// Email oder Name reicht. Telefon/Fax nur mit Firma zusammen. // Widerspruch: beide haben eine E-Mail, aber unterschiedlich -> verschiedene Personen.
return emailMatch || nameMatch || (phoneMatch && companyMatch) || (companyMatch && phoneMatch); bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch;
}
/// <summary> // Gleiche E-Mail ist der staerkste Identifikator und reicht allein.
/// Prueft ob zwei Felder kompatibel sind. if (emailMatch) return true;
/// 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; // Gleicher voller Name reicht, solange keine widerspruechliche E-Mail vorliegt.
if (aEmpty != bEmpty) return false; // Einer leer, anderer nicht // (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.)
return a.Equals(b, StringComparison.OrdinalIgnoreCase); if (nameMatch && !emailContradiction) return true;
}
private static bool PhoneFieldsCompatible(string a, string b) // Schwacher Pfad: Telefon/Fax nur zusammen mit gleicher Firma und ohne E-Mail-Widerspruch.
{ if (phoneMatch && companyMatch && !emailContradiction) return true;
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return true; return false;
if (aEmpty != bEmpty) return false;
return NormalizePhone(a) == NormalizePhone(b);
} }
private static string NormalizePhone(string phone) private static string NormalizePhone(string phone)
@@ -183,7 +149,21 @@ namespace StarfaceOutlookSync.Services
if (oc == null && sc != null) if (oc == null && sc != null)
{ {
// In Outlook geloescht -> in Starface auch loeschen // Outlook-Kontakt nicht gefunden.
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
var reMatch = FindMatch(sc, outlookContacts.Where(c =>
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
if (reMatch != null)
{
// Kontakt existiert noch in Outlook, nur EntryID geaendert
Log($" EntryID geaendert, verknuepfe neu: {sc.DisplayName}");
mapping.OutlookEntryId = reMatch.OutlookEntryId;
processedOutlookIds.Add(reMatch.OutlookEntryId);
newMappings.Add(mapping);
continue;
}
// Wirklich geloescht -> in Starface auch loeschen
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface) if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)
{ {
if (await starface.DeleteContactAsync(mapping.StarfaceId)) if (await starface.DeleteContactAsync(mapping.StarfaceId))
@@ -194,7 +174,6 @@ namespace StarfaceOutlookSync.Services
} }
else else
{ {
// Richtung erlaubt kein Loeschen -> Mapping behalten
newMappings.Add(mapping); newMappings.Add(mapping);
} }
continue; continue;
@@ -202,37 +181,50 @@ namespace StarfaceOutlookSync.Services
if (oc != null && sc == null) if (oc != null && sc == null)
{ {
// In Starface geloescht -> in Outlook auch loeschen // Starface-Kontakt nicht in der geladenen Liste.
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook) // Da unvollstaendige Ladevorgaenge inzwischen abgebrochen
{ // werden (siehe StarfaceApiClient), liegt das hoechstens an
if (_outlookService.DeleteContact(mapping.OutlookEntryId)) // einem anderen Adressbuch. NICHT loeschen und NICHT neu
{ // anlegen - sonst entstehen Dubletten. Mapping behalten,
result.Updated++; // beim naechsten Sync wird es erneut abgeglichen.
Log($" Geloescht (SF->OL): {oc.DisplayName}"); Log($" Starface-Kontakt nicht in Liste (anderes Adressbuch?), behalte Mapping: {oc.DisplayName}");
} newMappings.Add(mapping);
}
else
{
// Richtung erlaubt kein Loeschen -> Mapping behalten
newMappings.Add(mapping);
}
continue; continue;
} }
if (oc != null && sc != null) if (oc != null && sc != null)
{ {
// Beide vorhanden -> auf Aenderungen pruefen // Beide vorhanden -> auf Aenderungen pruefen.
// WICHTIG: jede Seite gegen ihre EIGENE Baseline pruefen.
// Outlook und Starface stellen denselben Kontakt
// unterschiedlich dar, ein gemeinsamer Hash schlaegt nie an.
var olHash = oc.GetHash(); var olHash = oc.GetHash();
var sfHash = sc.GetHash(); var sfHash = sc.GetHash();
bool olChanged = olHash != mapping.LastSyncHash;
bool sfChanged = sfHash != mapping.LastSyncHash; // Migration alter Mappings (nur LastSyncHash vorhanden):
// aktuellen Stand als Baseline uebernehmen und als synchron
// annehmen, damit kein Massen-Update ausgeloest wird.
if (string.IsNullOrEmpty(mapping.LastOutlookHash) &&
string.IsNullOrEmpty(mapping.LastStarfaceHash))
{
mapping.LastOutlookHash = olHash;
mapping.LastStarfaceHash = sfHash;
mapping.LastSyncHash = "";
newMappings.Add(mapping);
continue;
}
bool olChanged = olHash != mapping.LastOutlookHash;
bool sfChanged = sfHash != mapping.LastStarfaceHash;
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)) if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
{ {
// Outlook hat sich geaendert -> Starface updaten // Outlook hat sich geaendert -> Starface updaten
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
if (updated != null)
{ {
mapping.LastSyncHash = olHash; mapping.LastOutlookHash = olHash;
mapping.LastStarfaceHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Aktualisiert (OL->SF): {oc.DisplayName}"); Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
} }
@@ -240,30 +232,36 @@ namespace StarfaceOutlookSync.Services
else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)) else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook))
{ {
// Starface hat sich geaendert -> Outlook updaten // Starface hat sich geaendert -> Outlook updaten
if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
if (updated != null)
{ {
mapping.LastSyncHash = sfHash; mapping.LastStarfaceHash = sfHash;
mapping.LastOutlookHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Aktualisiert (SF->OL): {sc.DisplayName}"); Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
} }
} }
else if (olChanged && sfChanged) else if (olChanged && sfChanged)
{ {
// Beide geaendert -> Konflikt, neuere gewinnt (Outlook bevorzugt) // Beide geaendert -> Konflikt, Outlook bevorzugt
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook) if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
{ {
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
if (updated != null)
{ {
mapping.LastSyncHash = olHash; mapping.LastOutlookHash = olHash;
mapping.LastStarfaceHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Konflikt (OL gewinnt): {oc.DisplayName}"); Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
} }
} }
else else
{ {
if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
if (updated != null)
{ {
mapping.LastSyncHash = sfHash; mapping.LastStarfaceHash = sfHash;
mapping.LastOutlookHash = updated.GetHash();
result.Updated++; result.Updated++;
Log($" Konflikt (SF gewinnt): {sc.DisplayName}"); Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
} }
@@ -301,14 +299,16 @@ namespace StarfaceOutlookSync.Services
if (match != null) if (match != null)
{ {
// Existiert schon -> verknuepfen und updaten // Existiert schon -> verknuepfen und updaten
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook)) var updated = await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook);
if (updated != null)
{ {
newMappings.Add(new SyncMapping newMappings.Add(new SyncMapping
{ {
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId, OutlookEntryId = oc.OutlookEntryId,
StarfaceId = match.StarfaceId, StarfaceId = match.StarfaceId,
LastSyncHash = oc.GetHash() LastOutlookHash = oc.GetHash(),
LastStarfaceHash = updated.GetHash()
}); });
processedStarfaceIds.Add(match.StarfaceId); processedStarfaceIds.Add(match.StarfaceId);
unmappedStarface.Remove(match); unmappedStarface.Remove(match);
@@ -328,7 +328,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId, OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId, StarfaceId = created.StarfaceId,
LastSyncHash = oc.GetHash() LastOutlookHash = oc.GetHash(),
LastStarfaceHash = created.GetHash()
}); });
result.Created++; result.Created++;
Log($" Erstellt (OL->SF): {oc.DisplayName}"); Log($" Erstellt (OL->SF): {oc.DisplayName}");
@@ -374,14 +375,16 @@ namespace StarfaceOutlookSync.Services
if (match != null) if (match != null)
{ {
// Existiert schon -> verknuepfen und updaten // Existiert schon -> verknuepfen und updaten
if (_outlookService.UpdateContact(match.OutlookEntryId, sc)) var updated = _outlookService.UpdateContact(match.OutlookEntryId, sc);
if (updated != null)
{ {
newMappings.Add(new SyncMapping newMappings.Add(new SyncMapping
{ {
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = match.OutlookEntryId, OutlookEntryId = match.OutlookEntryId,
StarfaceId = sc.StarfaceId, StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash() LastStarfaceHash = sc.GetHash(),
LastOutlookHash = updated.GetHash()
}); });
processedOutlookIds.Add(match.OutlookEntryId); processedOutlookIds.Add(match.OutlookEntryId);
unmappedOutlook.Remove(match); unmappedOutlook.Remove(match);
@@ -400,7 +403,8 @@ namespace StarfaceOutlookSync.Services
ProfileId = profile.Id, ProfileId = profile.Id,
OutlookEntryId = created.OutlookEntryId, OutlookEntryId = created.OutlookEntryId,
StarfaceId = sc.StarfaceId, StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash() LastStarfaceHash = sc.GetHash(),
LastOutlookHash = created.GetHash()
}); });
result.Created++; result.Created++;
Log($" Erstellt (SF->OL): {sc.DisplayName}"); Log($" Erstellt (SF->OL): {sc.DisplayName}");
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle> <AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company> <Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product> <Product>Starface Outlook Sync</Product>
<Version>0.0.0.19</Version> <Version>0.0.0.24</Version>
<AssemblyVersion>0.0.0.19</AssemblyVersion> <AssemblyVersion>0.0.0.24</AssemblyVersion>
<FileVersion>0.0.0.19</FileVersion> <FileVersion>0.0.0.24</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description> <Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright> <Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.0.19", Text = "Version 0.0.0.24",
Left = 0, Top = 56, Width = 340, Height = 20, Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray ForeColor = Color.Gray
+33 -3
View File
@@ -22,6 +22,7 @@ namespace StarfaceOutlookSync.UI
private StatusStrip _statusBar; private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel; private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer; private Timer _autoSyncTimer;
private volatile bool _syncRunning = false;
public MainForm() public MainForm()
{ {
@@ -30,14 +31,31 @@ namespace StarfaceOutlookSync.UI
SetupAutoSync(); SetupAutoSync();
RefreshProfileList(); RefreshProfileList();
// Minimiert starten falls in Einstellungen aktiviert // Einstellungen laden und anwenden
var settings = UserSettings.Load(); var settings = UserSettings.Load();
settings.ApplyOutlookSecuritySetting();
if (settings.StartMinimized) if (settings.StartMinimized)
{ {
WindowState = FormWindowState.Minimized; WindowState = FormWindowState.Minimized;
ShowInTaskbar = false; ShowInTaskbar = false;
Visible = 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) protected override void SetVisibleCore(bool value)
@@ -58,8 +76,8 @@ namespace StarfaceOutlookSync.UI
private void InitializeComponent() private void InitializeComponent()
{ {
Text = "Starface Kontakt-Sync"; Text = "Starface Kontakt-Sync";
Size = new Size(620, 450); Size = new Size(830, 450);
MinimumSize = new Size(500, 350); MinimumSize = new Size(830, 350);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9); Font = new Font("Segoe UI", 9);
Icon = AppIcon.GetIcon(); Icon = AppIcon.GetIcon();
@@ -158,6 +176,7 @@ namespace StarfaceOutlookSync.UI
if (profiles.Any(p => p.Enabled)) if (profiles.Any(p => p.Enabled))
_trayMenu.Items.Add("-"); _trayMenu.Items.Add("-");
_trayMenu.Items.Add("Ueber", null, (s, e) => ShowAbout());
_trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication()); _trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication());
} }
@@ -318,6 +337,13 @@ namespace StarfaceOutlookSync.UI
private async Task RunSync(SyncProfile profile) private async Task RunSync(SyncProfile profile)
{ {
if (_syncRunning)
{
SetStatus("Sync laeuft bereits, bitte warten...");
return;
}
_syncRunning = true;
try try
{ {
SetStatus($"Synchronisiere '{profile.Name}'..."); SetStatus($"Synchronisiere '{profile.Name}'...");
@@ -340,6 +366,10 @@ namespace StarfaceOutlookSync.UI
ex.Message, ToolTipIcon.Error); ex.Message, ToolTipIcon.Error);
SetStatus($"Fehler: {ex.Message}"); SetStatus($"Fehler: {ex.Message}");
} }
finally
{
_syncRunning = false;
}
} }
private void SetStatus(string text) private void SetStatus(string text)
+34 -5
View File
@@ -6,7 +6,7 @@ namespace StarfaceOutlookSync.UI
{ {
public class SettingsForm : Form public class SettingsForm : Form
{ {
private CheckBox _chkStartMinimized; private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook;
private Button _btnSave, _btnCancel; private Button _btnSave, _btnCancel;
private readonly UserSettings _settings; private readonly UserSettings _settings;
@@ -19,7 +19,7 @@ namespace StarfaceOutlookSync.UI
private void InitializeComponent() private void InitializeComponent()
{ {
Text = "Einstellungen"; Text = "Einstellungen";
Size = new Size(350, 180); Size = new Size(380, 250);
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false; MaximizeBox = false;
MinimizeBox = false; MinimizeBox = false;
@@ -33,20 +33,47 @@ namespace StarfaceOutlookSync.UI
Checked = _settings.StartMinimized 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 hintText = "Hinweis: Outlook muss nach Aenderung neu gestartet werden.";
if (UserSettings.IsOutlookSecurityLockedByPolicy())
hintText += "\nAuf Domaenen-PCs: App einmalig als Admin starten!";
var lblHint = new Label
{
Text = hintText,
Left = 38, Top = 102, Width = 310, Height = 36,
ForeColor = UserSettings.IsOutlookSecurityLockedByPolicy() ? Color.OrangeRed : Color.Gray,
Font = new Font("Segoe UI", 8)
};
_btnSave = new Button _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 DialogResult = DialogResult.None
}; };
_btnSave.Click += (s, e) => Save(); _btnSave.Click += (s, e) => Save();
_btnCancel = new Button _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 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; AcceptButton = _btnSave;
CancelButton = _btnCancel; CancelButton = _btnCancel;
} }
@@ -54,6 +81,8 @@ namespace StarfaceOutlookSync.UI
private void Save() private void Save()
{ {
_settings.StartMinimized = _chkStartMinimized.Checked; _settings.StartMinimized = _chkStartMinimized.Checked;
_settings.SyncOnStart = _chkSyncOnStart.Checked;
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
_settings.Save(); _settings.Save();
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();