Keep in mind that if you delete a Contact, that's it. There is no audit trail and the data is lost forever. Doing that automatically based on input from an external source could lead to trouble (i.e. data being lost because of incorrect or unexpected input).
So another option that I would like to propose would be to create a "To be deleted" Address Book (you can name it whatever you like) and to not synchronize it anywhere and not add it to any Publications. Just leave it in the System Administration section.
Then write a small tool that uses the Import functionality of the API to move those Contacts to the new Address Book. Since they will be removed from their original Address Book into one that isn't synchronized anywhere, they will be removed from the website(s) too. And since you won't add it to any Publications, they won't be included in any Mailings (if you use Outbound E-mail).
Once you are confident that those Contacts can indeed be permanently deleted, you can then do so easily as they are all in the "To be deleted" Address Book. You can even do that automatically (see Saunders' answer) -- though again I would try to at least add some audit trail or extensive logging in case you need to recreate the Contact.
The import functionality works with CSV files and is pretty easy to use. I spent a few minutes creating this tool that does exactly what you are asking for, when given a file with the e-mail addresses of the Contacts to move:
using System;
using System.Globalization;
using Tridion.AudienceManagement.API;
using Tridion.AudienceManagement.API.Import;
namespace PseudoDelete
{
class Program
{
// Set this to the Key field of your "To be deleted" Address Book
private const string AddressBookKey = "TBR";
private static void Main()
{
Console.WriteLine($"Import started by {UserContext.Current.UserName}...");
UpdateContactsToRemove();
if (System.Diagnostics.Debugger.IsAttached)
{
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
}
private static void UpdateContactsToRemove()
{
ImportManager manager = new ImportManager(UserContext.Current);
// This file must be present in the ImportFolder configured in OutboundEmail.xml
ImportDefinition definition = manager.GetImportDefinition("ContactsExport.csv", ImportType.ImportContacts);
// Auto match the import file (Matches fields from the import file with equally named database fields).
definition.MatchImportFile();
// You have to set this to the 'All Contacts' Address Book in order to move Contacts across Address Books
definition.AddressBookUri = TcmUri.NullUri;
// Change the Address Book of the Contacts (i.e. move them)
definition.Mappings["group_keyvalue"].SourceField = null;
definition.Mappings["group_keyvalue"].DestinationField.DefaultValue = AddressBookKey;
// Hook into the feedback event to show some feedback
manager.Progress += ImportProgress;
// Import and report the results
ImportResult results = manager.ImportContacts(definition);
Console.WriteLine(results.AllDataImported ? "All Contacts were successfully imported" : string.Format(CultureInfo.InvariantCulture, "Contacts were imported, but some errors occurred, please check the {0} and {1} log files", definition.ErrorFile, definition.LogFile));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Imported {0} new contacts.", results.ContactsCreated));
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Updated {0} existing contacts.", results.ContactsUpdated));
}
private static void ImportProgress(object sender, ImportEventArgs e)
{
if (e.Progress > 0)
{
Console.WriteLine(string.Format("[{0}] > {1} rows copied)", e.Description, e.Progress));
}
else
{
Console.WriteLine(string.Format("[{0}] {1}: {2} {3}", DateTime.Now.ToString("s"), e.ImportStep.ToString().PadRight(22, ' '),
e.Description, e.Starting ? "(starting)" : "(complete)"));
}
}
}
}
If you have no control over the file and the columns aren't named the same as your fields, you'll need to map the column in the file to the field in the database. You do that similar to how we set the "group_keyvalue" field -- you just need to set the SourceField like this:
definition.Mappings["name-of-field-in-AM"].SourceField = definition.SourceFields["name-of-column-in-the-file"];
This approach will be very fast (I just moved 10.000 Contacts in about a second), whereas looping over them one by one will not be.
Good luck! :)