I am adding members to a group with the code below.
My question is very simple :
When adding members with this code, the invited person does not get an email notifying them. However when doing the same from the UI there is an option to notify the user. How can I do that from the code?
public void UpdateGoupMembers(string groupName, List<string> loginNames)
{
using (var clientContext = new ClientContext(baseUrl))
{
clientContext.Credentials = credentials;
var web = clientContext.Web;
var group = web.SiteGroups.GetByName(groupName);
if (group != null)
{
foreach (var loginName in loginNames)
{
var user = web.EnsureUser(loginName);
if (user != null)
{
group.Users.AddUser(user);
}
}
var existingUsers = group.Users;
clientContext.Load(existingUsers, includes => includes.Include(
f => f.LoginName,
f => f.UserId,
f => f.PrincipalType,
f => f.Email,
f => f.Id));
clientContext.ExecuteQuery();
foreach (var existingUser in existingUsers)
{
var userName = existingUser.LoginName.Split('|')[2];
if (!loginNames.Contains(userName))
{
group.Users.RemoveByLoginName(existingUser.LoginName);
}
}
}
clientContext.ExecuteQuery();
}
}
I do not believe it is possible to send a Welcome Email when users are added to a group programatically based off of the method documentation.
However, you can use the CSOM library to send an email programatically to the specific user after adding them to the group. Send Emails via SharePoint CSOM will be a great reference on how to do this.
I am using LyncClient library to create a widget and when a call comes in externally the remote participant sometimes comes up as 'sip:emailaddress#domain' if the contact is in the users outlook contacts.
Wondering if there is a way or library that allows me to open up the contact card for that email address and then get phone numbers if there are any.
Been pulling at my hair for a while now and can't figure it out. Any tips or experiences (good and bad) would be great! Let me know if you guys need more information.
I made a program that gets the phone address out of a SIP URL.
a SIP Url is basically in this format(Without quotes): "sip:username#domain"
try
{
LyncClient lyncClient = LyncClient.GetClient();
Contact contact;
List<object> endPoints = new List<object>();
Dictionary<string, string> phoneNumbers = new Dictionary<string, string>();
contact = lyncClient.ContactManager.GetContactByUri("sip:myusername#domain.com"); //PASS THE SIP ADDRESS HERE
var telephoneNumber = (List<object>)contact.GetContactInformation(ContactInformationType.ContactEndpoints);
//var contactName = contact.GetContactInformation(ContactInformationType.DisplayName).ToString();
//var availability = contact.GetContactInformation(ContactInformationType.Activity).ToString();
//foreach (object endPoint in telephoneNumber)
//{
//Console.WriteLine(((ContactEndpoint)endPoint).DisplayName + " " + ((ContactEndpoint)endPoint).Type.ToString());
//}
endPoints = telephoneNumber.Where<object>(N => ((ContactEndpoint)N).Type == ContactEndpointType.HomePhone || ((ContactEndpoint)N).Type == ContactEndpointType.MobilePhone || ((ContactEndpoint)N).Type == ContactEndpointType.OtherPhone || ((ContactEndpoint)N).Type == ContactEndpointType.WorkPhone).ToList<object>();
foreach (var endPoint in endPoints)
{
//Console.WriteLine(((ContactEndpoint)test).DisplayName.ToString());
string numberType = Regex.Replace(((ContactEndpoint)endPoint).Type.ToString(), #"Phone", "");
//string number = Regex.Replace(((ContactEndpoint)endPoint).DisplayName.ToString(), #"[^0-9]", "");
string number = "";
//Numbers only with dashes
if (Regex.IsMatch(((ContactEndpoint)endPoint).DisplayName.ToString(), #"^\d{3}-\d{3}-\d{4}$"))
{
number = ((ContactEndpoint)endPoint).DisplayName.ToString();
try
{
phoneNumbers.Add(numberType, number);
}
catch
{
}
}
//Console.WriteLine(numberType + " " + number);
}
foreach (var entry in phoneNumbers)
{
//entry.Key is the PhoneType
//entry.Value is the Phone Number
}
}
catch (Exception ex)
{
MessageBox.Show("An error occurred: " + ex.Message);
}
I don't think that this is the email address.
SIP URI's has the same format as an email address: sip:username#sipdomain, so maybe Lync is just sending the peer sip address.
In this case you just have to grab the sub-string between "sip:" and "#" to get the caller id.
Another problem is that there are multiple ways for SIP to send the caller id. Maybe you should look for Asserted/Preferred identity (and Lync just extracts it from the SIP "Contact" header).
I am using the Exchange Web Services Managed API 2.2 to monitor users inboxes and need to determine if an e-mail is a new item, a reply or a forwarded message.
I have seen various articles on SO such as how to notice if a mail is a forwarded mail? and Is there a way to determine if a email is a reply/response using ews c#? which both help in their specific cases but I still cannot work out how to distinguish between a reply and a forwarded item.
In the first article an extra 5 bytes is added each time (forward or reply) so I don't know what the last action was.
The second article suggests using the InReplyTo however when I examine the property for forwarded e-mails it contains the original senders e-mail address (not null).
I have seen other articles such as this or this that suggest using extended properties to examine the values in PR_ICON_INDEX, PR_LAST_VERB_EXECUTED and PR_LAST_VERB_EXECUTION_TIME.
My code looks as follows but never returns a value for lastVerbExecuted
var lastVerbExecutedProperty = new ExtendedPropertyDefinition(4225, MapiPropertyType.Integer);
var response = service.BindToItems(newMails, new PropertySet(BasePropertySet.IdOnly, lastVerbExecutedProperty));
var items = response.Select(itemResponse => itemResponse.Item);
foreach (var item in items)
{
object lastVerb;
if (item.TryGetProperty(lastVerbExecutedProperty, out lastVerb))
{
// do something
}
}
PR_ICON_INDEX, PR_LAST_VERB_EXECUTED and PR_LAST_VERB_EXECUTION_TIME would only work to tell you if the recipient has acted on a message in their Inbox. Eg if the user had replied or forwarded a message in their inbox then these properties get set on the message in their Inbox. On the message that was responded to or forwarded these properties would not be set. I would suggest you use the In-Reply-To Transport header which should be set on any message that is replied to or forwarded, this should contain the internet messageid of the message that was replied to or forwarded eg.
FindItemsResults<Item> fiRs = service.FindItems(WellKnownFolderName.Inbox, new ItemView(10));
PropertySet fiRsPropSet = new PropertySet(BasePropertySet.FirstClassProperties);
ExtendedPropertyDefinition PR_TRANSPORT_MESSAGE_HEADERS = new ExtendedPropertyDefinition(0x007D, MapiPropertyType.String);
fiRsPropSet.Add(PR_TRANSPORT_MESSAGE_HEADERS);
service.LoadPropertiesForItems(fiRs.Items, fiRsPropSet);
foreach (Item itItem in fiRs)
{
Object TransportHeaderValue = null;
if(itItem.TryGetProperty(PR_TRANSPORT_MESSAGE_HEADERS,out TransportHeaderValue)) {
string[] stringSeparators = new string[] { "\r\n" };
String[] taArray = TransportHeaderValue.ToString().Split(stringSeparators, StringSplitOptions.None);
for (Int32 txCount = 0; txCount < taArray.Length; txCount++)
{
if (taArray[txCount].Length > 12)
{
if (taArray[txCount].Substring(0, 12).ToLower() == "in-reply-to:")
{
String OriginalId = taArray[txCount].Substring(13);
Console.WriteLine(OriginalId);
}
}
}
}
}
Apart from the Subject prefix that was discussed in the other link I don't know of any other proprieties that will differentiate between a reply or forward.
Cheers
Glen
The best way to rely is on the ResponeCode of Extended properties
Refer below scripts
private static int IsForwardOrReplyMail(ExchangeService service, EmailMessage messageToCheck)
{
try
{
// Create extended property definitions for PidTagLastVerbExecuted and PidTagLastVerbExecutionTime.
ExtendedPropertyDefinition PidTagLastVerbExecuted = new ExtendedPropertyDefinition(0x1081, MapiPropertyType.Integer);
ExtendedPropertyDefinition PidTagLastVerbExecutionTime = new ExtendedPropertyDefinition(0x1082, MapiPropertyType.SystemTime);
PropertySet propSet = new PropertySet(BasePropertySet.IdOnly, EmailMessageSchema.Subject, PidTagLastVerbExecutionTime, PidTagLastVerbExecuted);
messageToCheck = EmailMessage.Bind(service, messageToCheck.Id, propSet);
// Determine the last verb executed on the message and display output.
object responseType;
messageToCheck.TryGetProperty(PidTagLastVerbExecuted, out responseType);
if (responseType != null && ((Int32)responseType) == 104)
{
//FORWARD
return 104;
}
else if (responseType != null && ((Int32)responseType) == 102)
{
//REPLY
return 102;
}
}
catch (Exception)
{
return 0;
//throw new NotImplementedException();
}
}
To determine if it was a reply to a email, you can use the EmailMessage objects InReplyTo property, e.g:
EmailMessage mail = ((EmailMessage)Item.Bind(service, new ItemId(UniqueId)));
if (mail.InReplyTo == null)
return;
else
..your code
The 'Users' on the network were moved to another domain recently. This caused a local method that gets the user's email address to break for a few of our web applications. This problem would likely fix itself after the web server is moved to the same domain, but would request help in fixing the code to allow this method to work in this split domain network (ex. Domain1.ad.com, Domain2.ad.com).
// Method below gets users email address.
public static string GetUserEmailAddress()
{
// Grab users network name. It will be in the form domain\username.
System.Security.Principal.WindowsPrincipal p = System.Threading.Thread.CurrentPrincipal as System.Security.Principal.WindowsPrincipal;
string NetworkID = p.Identity.Name;
// Remove the domain.
string[] networkIDparts = NetworkID.Split('\\');
string UserID = networkIDparts[1];
DirectorySearcher objsearch = new DirectorySearcher();
objsearch.Filter = "(& (cn="+ UserID.Trim() + ")(objectClass=user))";
objsearch.SearchScope = System.DirectoryServices.SearchScope.Subtree;
objsearch.PropertiesToLoad.Add("mail");
objsearch.PropertyNamesOnly = true ;
objsearch.Sort.Direction = System.DirectoryServices.SortDirection.Ascending;
SearchResultCollection colresults = objsearch.FindAll() ;
ArrayList arrGal = new ArrayList();
foreach(SearchResult objresult in colresults)
{
arrGal.Add(objresult.GetDirectoryEntry().Properties["mail"].Value);
}
objsearch.Dispose();
String[] myArray = (String[]) arrGal.ToArray(typeof(string));
if (myArray.Length == 0)
{ return "NoAddressAssigned#domain2.ad.com";}
else
{ return myArray[0] ;}
}
I'm also open to any ideas that would improve the functionality of this method.
I have an issue using c# on .Net 4 in a MVC web application, where when I query Active Directory, I frequently get an error: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014).
The strange thing is, that it will work flawlessly for a time, and then it will just start happening, and then just disappear again.
I have made a few modifications to the function to get it to work , but they all seem to fail. I am wondering if I am doing something wrong, or if there is a better way to do it.
Here is my current function, that will accept a loginId, and a PrincipalContext. The loginId can either be the user DisplayName i.e "John Smith", or DOMAINNAME\josmi. The default is to use the first 2 letters of their firstname, and then the first 3 letters of their surname. There is a check in there if this is not the case. This part if fine.
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(#"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
}
catch
{
if (tries > 5)
throw;
tries += 1;
System.Threading.Thread.Sleep(1000);
GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
This function is called like this:
MembershipGroups = ad.GetMemberGroups(user.SamAccountName, new PrincipalContext(ContextType.Domain));
The the error that I SOMETIMES get is:
System.AppDomainUnloadedException:
Attempted to access an unloaded
appdomain. (Exception from HRESULT:
0x80131014) at
System.StubHelpers.StubHelpers.InternalGetCOMHRExceptionObject(Int32
hr, IntPtr pCPCMD, Object pThis) at
System.StubHelpers.StubHelpers.GetCOMHRExceptionObject(Int32
hr, IntPtr pCPCMD, Object pThis) at
System.DirectoryServices.AccountManagement.UnsafeNativeMethods.IADsPathname.Retrieve(Int32
lnFormatType) at
System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDomainInfo()
at
System.DirectoryServices.AccountManagement.ADStoreCtx.get_UserSuppliedServerName()
at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.BuildPathFromDN(String
dn) at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNextPrimaryGroupDN()
at
System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNext()
at
System.DirectoryServices.AccountManagement.FindResultEnumerator1.MoveNext()
at
System.DirectoryServices.AccountManagement.FindResultEnumerator1.System.Collections.IEnumerator.MoveNext()
looking though reflector at System.DirectoryServices.AccountManagement the internal class "UnsafeNativeMethods" is implemented in native code, so UserSuppliedServerName one level up is all I can go on without looking at the CLR VM, (frankly im not sure even how to do that) Seems that a node is failing to return its primary group, so perhaps consider other implementations, after a bit of googling ive come across these that may help
Active Directory and nested groups this one may be promising heres the code sample..
public IList<string> FindUserGroupsLdap(string username)
{
// setup credentials and connection
var credentials = new NetworkCredential("username", "password", "domain");
var ldapidentifier = new LdapDirectoryIdentifier("server", 389, true, false);
var ldapConn = new LdapConnection(ldapidentifier, credentials);
// retrieving the rootDomainNamingContext, this will make sure we query the absolute root
var getRootRequest = new SearchRequest(string.Empty, "objectClass=*", SearchScope.Base, "rootDomainNamingContext");
var rootResponse = (SearchResponse)ldapConn.SendRequest(getRootRequest);
var rootContext = rootResponse.Entries[0].Attributes["rootDomainNamingContext"][0].ToString();
// retrieve the user
string ldapFilter = string.Format("(&(objectCategory=person)(sAMAccountName={0}))", username);
var getUserRequest = new SearchRequest(rootContext, ldapFilter, SearchScope.Subtree, null);
var userResponse = (SearchResponse)ldapConn.SendRequest(getUserRequest);
// send a new request to retrieve the tokenGroups attribute, we can not do this with our previous request since
// tokenGroups needs SearchScope.Base (dont know why...)
var tokenRequest = new SearchRequest(userResponse.Entries[0].DistinguishedName, "(&(objectCategory=person))", SearchScope.Base, "tokenGroups");
var tokenResponse = (SearchResponse)ldapConn.SendRequest(tokenRequest);
var tokengroups = tokenResponse.Entries[0].Attributes["tokenGroups"].GetValues(typeof(byte[]));
// build query string this query will then look like (|(objectSid=sid)(objectSid=sid2)(objectSid=sid3))
// we need to convert the given bytes to a hexadecimal representation because thats the way they
// sit in ActiveDirectory
var sb = new StringBuilder();
sb.Append("(|");
for (int i = 0; i < tokengroups.Length; i++)
{
var arr = (byte[])tokengroups[i];
sb.AppendFormat("(objectSid={0})", BuildHexString(arr));
}
sb.Append(")");
// send the request with our build query. This will retrieve all groups with the given objectSid
var groupsRequest = new SearchRequest(rootContext, sb.ToString(), SearchScope.Subtree, "sAMAccountName");
var groupsResponse = (SearchResponse)ldapConn.SendRequest(groupsRequest);
// loop trough and get the sAMAccountName (normal, readable name)
var userMemberOfGroups = new List<string>();
foreach (SearchResultEntry entry in groupsResponse.Entries)
userMemberOfGroups.Add(entry.Attributes["sAMAccountName"][0].ToString());
return userMemberOfGroups;
}
private string BuildHexString(byte[] bytes)
{
var sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
sb.AppendFormat("\\{0}", bytes[i].ToString("X2"));
return sb.ToString();
}
These are more for info purposes
How to use the PrimaryGroupID attribute to find the primary group for a user
Determining User Group Membership in Active Directory and ADAM
I don't know how PrincipalContext is being passed in, here, but one thing I noticed in my own code and research when I had this error, I had:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain);
UserPrincipal oUserPrincipal = UserPrincipal.FindByIdentity(oPrincipalContext , strUserName);
Where strUserName was some user, i.e. DOMAIN\johndoe
I was calling that code (which was in a separate function) and returning the UserPrincipal object as up and passing it to:
using (PrincipalSearchResult<Principal> result = up.GetGroups())
{
// do something with result, here
}
result wouldn't be null, but after I checked for that condition, I checked if result.Count() > 0, and that's when it would fail (sometimes - though I could re-create the conditions when it would happen by clicking on a particular tab in my app that called this code - even though the same code was called onload of my app and had no issues). The Message property in result was Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014).
I found in a similar post to this one that all I had to do was specify the domain in my PrincipalContext. Since I could not hard code mine in, as we move our code between Dev, Test, and Production environments where they have different domains for each of these, I was able to specify it as Environment.UserDomainName:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain, Environment.UserDomainName);
This got rid of the error, for me.
This issue is the same as Determine if user is in AD group for .NET 4.0 application
It appears to be a bug in ADSI that was resolved with a hotfix. Windows 7 SP1 and Windows Server 2008 R2 SP1 don't include the fix, so it will need to be manually deployed on your development machines and server environments.
http://support.microsoft.com/kb/2683913
You could put in some logging to narrow down the problem. That Thread.Sleep does not look like something one would want in a web application :)
If you are getting exceptions maybe you could handle them differently.
I reckon your AppDomain is being recycled while AD is doing its voodoo. Adding logging to the Application_End could also provide some clues.
try
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
bool Done = false;
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(#"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
Done = true;
}
catch
{
if (tries > 5)
throw;
tries += 1;
}
if ( ( !Done) && (tries < 6) )
{
System.Threading.Thread.Sleep(1000);
result = GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
if ( principal == null )
return;
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
When an exception happens you called the function again from the catch-block (depending on the value of tries) but discarded its return value - so even if the second/third... call worked you returned an empty result to the original caller.
I changed that so the result won't be discarded anymore...
In the second function you never checked the principal param for null before starting the foreach... I changed that too...
And I removed the recursion from within the catch block catch (although I am really not sure whether this change has any real effect).