Only notify clients actually affected by a conflict
Bisher bekam jeder Arbeitsplatz, der den Konflikt-Kontakt im Adressbuch hat, den Hinweis - auch wenn er den Wert gar nicht selbst gepflegt hat. Jetzt zeigt ein Client eine fremde Konflikt-Notiz nur, wenn er den Kontakt hat UND sein eigener Feldwert vom uebernommenen (Gewinner-)Wert abweicht. Die Pruefung laeuft VOR dem Sync (gegen den eigenen Mapping-Snapshot), bevor der Sync den Stand auf den Gewinner-Wert angleicht. - FieldConflict/ConflictNotice: stabiler FieldKey zusaetzlich zum Anzeige-Label. - ContactMerger: GetValue/ValuesEqual per FieldKey (telefon-normalisiert). - ConflictNotifier.GetPending: Filter "eigener Wert != Gewinner-Wert", bekommt StarfaceId -> eigener Kontaktstand. - MainForm zeigt die Hinweise jetzt vor dem Sync und liefert den eigenen Stand aus den Mapping-Snapshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -40,11 +40,12 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
|
|||||||
|
|
||||||
- **Clientuebergreifende Konflikt-Hinweise (Mehrplatz).** Wird ein echter
|
- **Clientuebergreifende Konflikt-Hinweise (Mehrplatz).** Wird ein echter
|
||||||
Feld-Konflikt aufgeloest, legt der Client eine Notiz im gemeinsamen Verzeichnis
|
Feld-Konflikt aufgeloest, legt der Client eine Notiz im gemeinsamen Verzeichnis
|
||||||
(`conflicts/`) ab - verschluesselt nach Kontakt (StarfaceId). Jeder andere
|
(`conflicts/`) ab - zugeordnet nach Kontakt (StarfaceId). Ein anderer
|
||||||
Arbeitsplatz, der denselben Kontakt pflegt, bekommt die Meldung beim naechsten
|
Arbeitsplatz zeigt die Meldung beim naechsten Sync nur an, wenn er den Kontakt
|
||||||
Sync als Tray-Hinweis angezeigt (auch der, dessen Wert ueberschrieben wurde).
|
selbst hat UND sein eigener Feldwert vom uebernommenen Wert abweicht - so wird
|
||||||
Bereits gezeigte Notizen merkt sich jeder Client lokal; alte Notizen werden
|
der ueberschriebene Arbeitsplatz gewarnt, waehrend bereits aktuelle Clients
|
||||||
nach 7 Tagen aufgeraeumt.
|
nicht benachrichtigt werden. Bereits gezeigte Notizen merkt sich jeder Client
|
||||||
|
lokal; alte Notizen werden nach 7 Tagen aufgeraeumt.
|
||||||
- **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst
|
- **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst
|
||||||
sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein
|
sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein
|
||||||
Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere
|
Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere
|
||||||
|
|||||||
@@ -301,11 +301,12 @@ umgekehrt), wird der zweite Lauf uebersprungen. Atomar per `Interlocked`.
|
|||||||
**Konflikt-Hinweise:** Wird im bidirektionalen Modus ein echter Feld-Konflikt
|
**Konflikt-Hinweise:** Wird im bidirektionalen Modus ein echter Feld-Konflikt
|
||||||
aufgeloest (dasselbe Feld auf beiden Seiten unterschiedlich geaendert, Outlook
|
aufgeloest (dasselbe Feld auf beiden Seiten unterschiedlich geaendert, Outlook
|
||||||
gewinnt), legt der Client eine Notiz im Unterordner `conflicts/` des gemeinsamen
|
gewinnt), legt der Client eine Notiz im Unterordner `conflicts/` des gemeinsamen
|
||||||
Verzeichnisses ab - zugeordnet ueber die StarfaceId des Kontakts. Andere
|
Verzeichnisses ab - zugeordnet ueber die StarfaceId des Kontakts. Beim naechsten
|
||||||
Arbeitsplaetze, die denselben Kontakt pflegen, zeigen die Meldung beim naechsten
|
Sync zeigt ein anderer Arbeitsplatz die Meldung als Tray-Hinweis - aber **nur,
|
||||||
Sync als Tray-Hinweis an (so erfaehrt auch der Arbeitsplatz, dessen Wert
|
wenn er den Kontakt selbst hat UND sein eigener Feldwert vom uebernommenen Wert
|
||||||
ueberschrieben wurde, davon). Gezeigte Notizen merkt sich jeder Client lokal,
|
abweicht** (so erfaehrt der ueberschriebene Arbeitsplatz davon, waehrend Clients,
|
||||||
veraltete Notizen werden nach 7 Tagen entfernt.
|
die den Wert ohnehin schon haben, nicht benachrichtigt werden). Gezeigte Notizen
|
||||||
|
merkt sich jeder Client lokal, veraltete Notizen werden nach 7 Tagen entfernt.
|
||||||
|
|
||||||
> Hinweis: Im Mehrplatz-Betrieb sollte die **bidirektionale** Sync-Richtung
|
> Hinweis: Im Mehrplatz-Betrieb sollte die **bidirektionale** Sync-Richtung
|
||||||
> verwendet werden. Die Ein-Richtungs-Modi ("Ersetzen") wuerden die von anderen
|
> verwendet werden. Die Ein-Richtungs-Modi ("Ersetzen") wuerden die von anderen
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace StarfaceOutlookSync.Models
|
|||||||
public string StarfaceId { get; set; } = "";
|
public string StarfaceId { get; set; } = "";
|
||||||
public string ContactName { get; set; } = "";
|
public string ContactName { get; set; } = "";
|
||||||
public string Field { get; set; } = "";
|
public string Field { get; set; } = "";
|
||||||
|
public string FieldKey { get; set; } = "";
|
||||||
public string OutlookValue { get; set; } = "";
|
public string OutlookValue { get; set; } = "";
|
||||||
public string StarfaceValue { get; set; } = "";
|
public string StarfaceValue { get; set; } = "";
|
||||||
public string Winner { get; set; } = "";
|
public string Winner { get; set; } = "";
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ namespace StarfaceOutlookSync.Models
|
|||||||
{
|
{
|
||||||
public string StarfaceId { get; set; } = "";
|
public string StarfaceId { get; set; } = "";
|
||||||
public string ContactName { get; set; } = "";
|
public string ContactName { get; set; } = "";
|
||||||
public string Field { get; set; } = "";
|
public string Field { get; set; } = ""; // Anzeige-Label, z.B. "E-Mail"
|
||||||
|
public string FieldKey { get; set; } = ""; // stabiler Schluessel, z.B. "Email"
|
||||||
public string OutlookValue { get; set; } = "";
|
public string OutlookValue { get; set; } = "";
|
||||||
public string StarfaceValue { get; set; } = "";
|
public string StarfaceValue { get; set; } = "";
|
||||||
public string Winner { get; set; } = ""; // "Outlook" oder "Starface"
|
public string Winner { get; set; } = ""; // "Outlook" oder "Starface"
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
StarfaceId = c.StarfaceId,
|
StarfaceId = c.StarfaceId,
|
||||||
ContactName = c.ContactName,
|
ContactName = c.ContactName,
|
||||||
Field = c.Field,
|
Field = c.Field,
|
||||||
|
FieldKey = c.FieldKey,
|
||||||
OutlookValue = c.OutlookValue,
|
OutlookValue = c.OutlookValue,
|
||||||
StarfaceValue = c.StarfaceValue,
|
StarfaceValue = c.StarfaceValue,
|
||||||
Winner = c.Winner
|
Winner = c.Winner
|
||||||
@@ -74,10 +75,13 @@ namespace StarfaceOutlookSync.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Liefert ungesehene Konflikt-Notizen, die Kontakte dieses Clients
|
/// Liefert ungesehene Konflikt-Notizen, die Kontakte dieses Clients
|
||||||
/// betreffen (StarfaceId in myStarfaceIds) und nicht von ihm selbst
|
/// betreffen und nicht von ihm selbst stammen. Es werden nur die Notizen
|
||||||
/// stammen. Markiert sie als gesehen und raeumt veraltete Notizen auf.
|
/// zurueckgegeben, bei denen der EIGENE aktuelle Feldwert vom uebernommenen
|
||||||
|
/// (Gewinner-)Wert abweicht - wer den Wert ohnehin schon hat, wird nicht
|
||||||
|
/// benachrichtigt. myContacts bildet StarfaceId -> eigener Kontaktstand ab.
|
||||||
|
/// Markiert verarbeitete Notizen als gesehen und raeumt veraltete auf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ConflictNotice> GetPending(string sharedDir, ICollection<string> myStarfaceIds)
|
public List<ConflictNotice> GetPending(string sharedDir, IDictionary<string, UnifiedContact> myContacts)
|
||||||
{
|
{
|
||||||
var result = new List<ConflictNotice>();
|
var result = new List<ConflictNotice>();
|
||||||
if (string.IsNullOrWhiteSpace(sharedDir)) return result;
|
if (string.IsNullOrWhiteSpace(sharedDir)) return result;
|
||||||
@@ -117,9 +121,29 @@ namespace StarfaceOutlookSync.Services
|
|||||||
seen.Add(n.Id); // selbst erzeugt -> nicht erneut anzeigen
|
seen.Add(n.Id); // selbst erzeugt -> nicht erneut anzeigen
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (myStarfaceIds != null && !string.IsNullOrEmpty(n.StarfaceId)
|
|
||||||
&& !myStarfaceIds.Contains(n.StarfaceId))
|
// Habe ich diesen Kontakt ueberhaupt?
|
||||||
continue; // betrifft mich nicht
|
UnifiedContact mine = null;
|
||||||
|
bool haveContact = !string.IsNullOrEmpty(n.StarfaceId)
|
||||||
|
&& myContacts != null
|
||||||
|
&& myContacts.TryGetValue(n.StarfaceId, out mine);
|
||||||
|
if (!haveContact)
|
||||||
|
continue; // betrifft mich nicht (kein seen -> evtl. spaeter relevant)
|
||||||
|
|
||||||
|
// Bin ich wirklich betroffen? Nur wenn mein aktueller Feldwert vom
|
||||||
|
// uebernommenen Wert abweicht. Wer den Gewinner-Wert schon hat, wird
|
||||||
|
// nicht benachrichtigt.
|
||||||
|
if (mine != null && !string.IsNullOrEmpty(n.FieldKey))
|
||||||
|
{
|
||||||
|
var winnerValue = string.Equals(n.Winner, "Outlook", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? n.OutlookValue : n.StarfaceValue;
|
||||||
|
var myValue = ContactMerger.GetValue(mine, n.FieldKey);
|
||||||
|
if (ContactMerger.ValuesEqual(n.FieldKey, myValue, winnerValue))
|
||||||
|
{
|
||||||
|
seen.Add(n.Id); // nicht betroffen -> als erledigt merken
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.Add(n);
|
result.Add(n);
|
||||||
seen.Add(n.Id);
|
seen.Add(n.Id);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
{
|
{
|
||||||
private class FieldDef
|
private class FieldDef
|
||||||
{
|
{
|
||||||
|
public string Key;
|
||||||
public string Label;
|
public string Label;
|
||||||
public Func<UnifiedContact, string> Get;
|
public Func<UnifiedContact, string> Get;
|
||||||
public Action<UnifiedContact, string> Set;
|
public Action<UnifiedContact, string> Set;
|
||||||
@@ -23,28 +24,43 @@ namespace StarfaceOutlookSync.Services
|
|||||||
|
|
||||||
private static readonly FieldDef[] Fields = new[]
|
private static readonly FieldDef[] Fields = new[]
|
||||||
{
|
{
|
||||||
new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v },
|
new FieldDef { Key = "FirstName", Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v },
|
||||||
new FieldDef { Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v },
|
new FieldDef { Key = "LastName", Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v },
|
||||||
new FieldDef { Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v },
|
new FieldDef { Key = "Company", Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v },
|
||||||
new FieldDef { Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v },
|
new FieldDef { Key = "JobTitle", Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v },
|
||||||
new FieldDef { Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v },
|
new FieldDef { Key = "Email", Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v },
|
||||||
new FieldDef { Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v },
|
new FieldDef { Key = "EmailSecondary", Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v },
|
||||||
new FieldDef { Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true },
|
new FieldDef { Key = "PhoneWork", Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true },
|
||||||
new FieldDef { Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true },
|
new FieldDef { Key = "PhoneMobile", Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true },
|
||||||
new FieldDef { Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true },
|
new FieldDef { Key = "PhoneHome", Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true },
|
||||||
new FieldDef { Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true },
|
new FieldDef { Key = "Fax", Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true },
|
||||||
new FieldDef { Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v },
|
new FieldDef { Key = "Street", Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v },
|
||||||
new FieldDef { Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v },
|
new FieldDef { Key = "City", Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v },
|
||||||
new FieldDef { Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v },
|
new FieldDef { Key = "PostalCode", Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v },
|
||||||
new FieldDef { Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v },
|
new FieldDef { Key = "State", Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v },
|
||||||
new FieldDef { Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v },
|
new FieldDef { Key = "Country", Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v },
|
||||||
new FieldDef { Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v },
|
new FieldDef { Key = "Website", Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v },
|
||||||
new FieldDef { Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v },
|
new FieldDef { Key = "Notes", Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v },
|
||||||
new FieldDef { Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v },
|
new FieldDef { Key = "Salutation", Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v },
|
||||||
new FieldDef { Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v },
|
new FieldDef { Key = "Title", Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v },
|
||||||
new FieldDef { Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
|
||||||
|
public static string GetValue(UnifiedContact c, string key)
|
||||||
|
{
|
||||||
|
if (c == null || string.IsNullOrEmpty(key)) return "";
|
||||||
|
var f = Fields.FirstOrDefault(x => x.Key == key);
|
||||||
|
return f == null ? "" : (f.Get(c) ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Vergleicht zwei Feldwerte (telefon-normalisiert je nach Feld).</summary>
|
||||||
|
public static bool ValuesEqual(string key, string a, string b)
|
||||||
|
{
|
||||||
|
var f = Fields.FirstOrDefault(x => x.Key == key);
|
||||||
|
return Equal(a ?? "", b ?? "", f?.IsPhone ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
|
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
|
||||||
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
|
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
|
||||||
@@ -95,6 +111,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
StarfaceId = starface.StarfaceId,
|
StarfaceId = starface.StarfaceId,
|
||||||
ContactName = outlook.DisplayName,
|
ContactName = outlook.DisplayName,
|
||||||
Field = f.Label,
|
Field = f.Label,
|
||||||
|
FieldKey = f.Key,
|
||||||
OutlookValue = olv,
|
OutlookValue = olv,
|
||||||
StarfaceValue = sfv,
|
StarfaceValue = sfv,
|
||||||
Winner = outlookWins ? "Outlook" : "Starface"
|
Winner = outlookWins ? "Outlook" : "Starface"
|
||||||
|
|||||||
@@ -361,6 +361,11 @@ namespace StarfaceOutlookSync.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Konflikt-Hinweise von ANDEREN Arbeitsplaetzen anzeigen, BEVOR der
|
||||||
|
// Sync den eigenen Stand auf den Gewinner-Wert aktualisiert (sonst
|
||||||
|
// koennte man nicht mehr erkennen, ob man betroffen war).
|
||||||
|
ShowRemoteConflictNotices(sharedDir);
|
||||||
|
|
||||||
SetStatus($"Synchronisiere '{profile.Name}'...");
|
SetStatus($"Synchronisiere '{profile.Name}'...");
|
||||||
_trayIcon.ShowBalloonTip(2000, "Starface Sync",
|
_trayIcon.ShowBalloonTip(2000, "Starface Sync",
|
||||||
$"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info);
|
$"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info);
|
||||||
@@ -388,9 +393,6 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_conflictNotifier.Write(sharedDir, result.Conflicts);
|
_conflictNotifier.Write(sharedDir, result.Conflicts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konflikt-Hinweise von ANDEREN Arbeitsplaetzen (zu meinen Kontakten) anzeigen.
|
|
||||||
ShowRemoteConflictNotices(sharedDir);
|
|
||||||
|
|
||||||
SetStatus(msg);
|
SetStatus(msg);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -456,7 +458,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
if (string.IsNullOrWhiteSpace(sharedDir)) return;
|
if (string.IsNullOrWhiteSpace(sharedDir)) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pending = _conflictNotifier.GetPending(sharedDir, AllMappedStarfaceIds());
|
var pending = _conflictNotifier.GetPending(sharedDir, MyContactsByStarfaceId());
|
||||||
if (pending.Count == 0) return;
|
if (pending.Count == 0) return;
|
||||||
|
|
||||||
var detail = string.Join("\n", pending.Take(5).Select(p => p.ToString()));
|
var detail = string.Join("\n", pending.Take(5).Select(p => p.ToString()));
|
||||||
@@ -468,19 +470,23 @@ namespace StarfaceOutlookSync.UI
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Alle von diesem Client gemappten StarfaceIds (ueber alle Profile).</summary>
|
/// <summary>
|
||||||
private HashSet<string> AllMappedStarfaceIds()
|
/// Eigener Kontaktstand je StarfaceId (ueber alle Profile), aus den
|
||||||
|
/// Mapping-Snapshots. Dient dazu, bei Konflikt-Notizen zu pruefen, ob der
|
||||||
|
/// eigene Feldwert ueberhaupt vom uebernommenen Wert abweicht.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, UnifiedContact> MyContactsByStarfaceId()
|
||||||
{
|
{
|
||||||
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var map = new Dictionary<string, UnifiedContact>(StringComparer.OrdinalIgnoreCase);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var p in _profileManager.GetProfiles())
|
foreach (var p in _profileManager.GetProfiles())
|
||||||
foreach (var m in _profileManager.GetMappings(p.Id))
|
foreach (var m in _profileManager.GetMappings(p.Id))
|
||||||
if (!string.IsNullOrEmpty(m.StarfaceId))
|
if (!string.IsNullOrEmpty(m.StarfaceId) && !map.ContainsKey(m.StarfaceId))
|
||||||
ids.Add(m.StarfaceId);
|
map[m.StarfaceId] = m.LastOutlook;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
return ids;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetStatus(string text)
|
private void SetStatus(string text)
|
||||||
|
|||||||
Reference in New Issue
Block a user