Hi All,

Can I use CURL in multi threaded application?

My Requirement is as follows:
I have to post the data to the server. suppose if the connection is broken
with the server, i need to store the data and post it when it comes online.
So this activity will happen on back ground (on a separate thread). My main
thread will keep trying to post the data normally. My other thread will
check the connection with the host and post the data.

To test this scenario, I have written the sample code. To simulate this
scenirio, during running of the application i am pulling my ethernet cable
so that i can simulate the connection break down case. Both threads giving
successful response till i pulled the cable. Once i pull the cable and
connect it back, one of the threads which is checking the connection is
keep getting  CURLE_COULDNT_RESOLVE_HOST error even though other thread is
successful in posting the data to the host.

Please refer to attached sample code(curlLibraryTest.c):
In the Sample code, we have main thread and waitForHostConnection thread.
Main thread will keep posting the data to the host at regular intervals.
waitForHostConnection  thread will keep checking the host connection.

Both threads are working fine and giving succesful response till i pulled
the ethernet cable, once i pulled and put the cable back,
waitForHostConnection thread is giving CURLE_COULDNT_RESOLVE_HOST error.

When i am doing both things posting and checking connection in the same
thread(main) then it works fine even after pulling and connecting the cable
back. When i move connection checking part to the separate thread then the
problem starts.

Could anyone help me to resolve this.

Thank you for your time and help.

Regards
Praveen
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
#include <curl/easy.h>
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <syslog.h>
#include <pthread.h>

#define LOGS "/mnt/flash/logs/usr1/curlTestApp.log"

#define TRUE 1
#define FALSE 0

# define SUCCESS 0
# define FAILURE -1

typedef enum
{
	PRIMARY_URL = 0,
	SECONDARY_URL
}
HOST_URL_ENUM;

static pthread_t	threadID;

#define CA_CERT_FILE	"ca-bundle.crt"

FILE*		fptr			= NULL;
FILE*		fptr2			= NULL;
/*
 * ----------------------------------------------------------------------------
 * Some certificate information:
 *
 * PEM Format: .pem, .crt, .cer and .key
 *
 * DER Format:
 * ----------------------------------------------------------------------------
 */
static struct curl_slist *	headers			= NULL;
	char		szMsg[256]		= "";


typedef struct
{
	int		iTotSize;
	int		iWritten;
	char *	szMsg;
}
RESP_DATA_STYPE, * RESP_DATA_PTYPE;

static int initSSICURLHandle(CURL **, char*);
static void commonInit(CURL *, int);
static int sendDataToHost(CURL *, char *, int, char **, int *);
static size_t saveResponse(void *, size_t, size_t, void *);
static int initializeRespData(RESP_DATA_PTYPE);
static void* waitForHostConnection(void *);
static int chkConn(CURL *);

int main(int argc, char *argv[])
{
	int 		iRetVal			= SUCCESS;
 	int			rv				= SUCCESS;
	int			iCnt			= 0;
	char*		szHostUrl		= "https://sample.com";;
	CURL *		curlHandle1		= NULL;
	CURL *		curlHandle2		= NULL;
	char		req[2560]		= "Sample Data";
	int			reqSize			= strlen(req);
	char*		resp			= NULL;
	int			respSize		= 0;
	int	bIsHostWentOffline = FALSE;
	
	fptr = fopen (LOGS, "w");

	/* Add data to the headers */
	headers = curl_slist_append(headers, "Content-Type:text/xml");


	fprintf(fptr, "%s: Entered..\n", __FUNCTION__);

	
	fprintf (fptr, "%s: Initializing the curl handle for the first one\n", __FUNCTION__);
	fclose(fptr);

	rv = initSSICURLHandle(&curlHandle1, szHostUrl);

	rv = pthread_create(&threadID, NULL, waitForHostConnection, &bIsHostWentOffline);
	if(rv != SUCCESS)
	{
		fprintf(fptr, "%s: FAILED to start waitForHostConnection thread",
															__FUNCTION__);
	}

	while(1)
	{
		fptr = fopen (LOGS, "a");


		fprintf (fptr, "%s: Posting data to first Curl\n", __FUNCTION__, rv);
		/*
		 * CURL Handler was initialized successfully
		 */
		fprintf(fptr, "Request: %s\n", req);

		rv = sendDataToHost(curlHandle1, req, reqSize, &resp, &respSize);
		if(rv != SUCCESS)
		{
			fprintf(fptr, "%s: Failed to post data to server\n", __FUNCTION__);
			
			curl_easy_cleanup(curlHandle1);
			
			curlHandle1 = NULL;
			
			rv = initSSICURLHandle(&curlHandle1, szHostUrl);
			if(rv == SUCCESS)
			{
				fprintf(fptr, "%s: Timed out Error from PWC\n", __FUNCTION__);
				fprintf (fptr,"%s: Reinitialized the connection\n", __FUNCTION__);
			}
		}


		if (resp != NULL)
		{
			fprintf (fptr, "Response Received: %s \n", resp);
			free(resp);
			resp = NULL;
		}
		sleep(3);
		

		fclose(fptr);
	}
	
	fprintf(fptr, "%s: Returning..\n", __FUNCTION__);

	
	return rv;
}

/*
 * ============================================================================
 * Function Name: initSSICURLHandle
 *
 * Description	:
 *
 * Input Params	:
 *
 * Output Params: SUCCESS / FAILURE
 * ============================================================================
 */
static int initSSICURLHandle(CURL ** handle, char* pszHostUrl)
{
	int					rv				= SUCCESS;
	char				szTmpURL[100]	= "";
	CURL *				locHandle		= NULL;
	int					certValdReq 	= TRUE;

	fprintf(fptr,   "%s: --- enter ---\n", __FUNCTION__);

	 

	while(1)
	{
		/* Initialize the handle using the CURL library */
		locHandle = curl_easy_init();
		if(locHandle == NULL)
		{
			fprintf(fptr,   "%s: Curl initialization for handle failed\n",
															__FUNCTION__);


			rv = FAILURE;
			break;
		}


		curl_easy_setopt(locHandle, CURLOPT_URL, pszHostUrl);

		fprintf(fptr,   "%s: Setting PWC URL = [%s]\n", __FUNCTION__, pszHostUrl);


		/* Set the connection timeout */
		curl_easy_setopt(locHandle, CURLOPT_CONNECTTIMEOUT, 10);

		/* Set the total timeout */

		curl_easy_setopt(locHandle, CURLOPT_TIMEOUT, 20);


		/* Set the other common features */
		commonInit(locHandle, certValdReq);
		*handle = locHandle;

		break;
	}

	fprintf(fptr,   "%s: Returning [%d]\n", __FUNCTION__, rv);


	return rv;
}

/*
 * ============================================================================
 * Function Name: commonInit
 *
 * Description	: This function contains the code for common initialization of
 * 					all the three handles needed for communication with the PWC
 *
 * Input Params	: CURL * handle	-> curl handle that needs to be configured
 *
 * Output Params: void
 * ============================================================================
 */
static void commonInit(CURL * handle, int certValdReq)
{
	
	fprintf(fptr,   "%s: --- enter ---\n", __FUNCTION__);


	/* Set the NO SIGNAL option, This option is very important to set in case
	 * of multi-threaded applications like ours, because otherwise the libcurl
	 * uses signal handling for the communication and that causes the threads
	 * to go crazy and even crash. Fix done for issue 1659 (Church of LDS AmDocs
	 * Case# 130826-3983) */
	curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);

	/* Set the HTTP post option */
	curl_easy_setopt(handle, CURLOPT_POST, 1L);

	/* Add the headers */
	curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);

/*#ifdef DEBUG
	/* Add the debug function 
	curl_easy_setopt(handle, CURLOPT_DEBUGFUNCTION, curlDbgFunc);
#endif*/

	if (certValdReq == TRUE)
	{
		fprintf(fptr,  "%s: Url starts with https. Doing certificate validation\n", __FUNCTION__);
				

		curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
		curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L);
	}

	/* Set the CA certificate */
	curl_easy_setopt(handle, CURLOPT_CAINFO, CA_CERT_FILE);

	/* Set the write function */
	curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, saveResponse);

	/* Set the detection as TRUE for HTTP errors */
	curl_easy_setopt(handle, CURLOPT_FAILONERROR, TRUE);

	fprintf(fptr,   "%s: --- returning ---\n", __FUNCTION__);

	return;
}

/*
 * ============================================================================
 * Function Name: sendDataToHost
 *
 * Description	: This function sends the XML data to the PWC server, using the
 * 					appropriate CURL handle.
 *
 * Input Params	:
 * 				PWC_URL_TYPE_ENUM urlType -> constant telling which handle to
 * 												use
 * 				unsigned char *	  data	  -> the XML data that needs to be sent
 * 				int				  size	  -> size of the data
 *
 * Output Params: SUCCESS / FAILURE
 * ============================================================================
 */
static int sendDataToHost(CURL * curlHandle, char * req, int reqSize,
												char ** resp, int * respSize)
{
	int				rv				   = SUCCESS;
	char			szCurlErrBuf[4096] = "";
	RESP_DATA_STYPE	stRespData;

#ifdef DEVDEBUG
	char			fptr[4096+256]	= "";
#elif DEBUG
	char			fptr[256]	= "";
#endif

	fprintf(fptr,   "%s: --- enter ---\n", __FUNCTION__);

	 

	while(1)
	{
		/* Initialize the response data */
		rv = initializeRespData(&stRespData);
		if(rv != SUCCESS)
		{
			fprintf(fptr,   "%s: Initialization of response data FAILED\n"
																, __FUNCTION__);

			 

			break;
		}

		/* Set some curl library options before sending the data */
		curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, &stRespData);
		curl_easy_setopt(curlHandle, CURLOPT_POSTFIELDS, (void *) req);
		curl_easy_setopt(curlHandle, CURLOPT_POSTFIELDSIZE, reqSize);
		curl_easy_setopt(curlHandle, CURLOPT_ERRORBUFFER, szCurlErrBuf);

		fprintf(fptr,   "%s: Posting the data to the server\n", __FUNCTION__);

		 

		/* post the data to the server */
		rv = curl_easy_perform(curlHandle);

		fprintf(fptr,   "%s: curl_easy_perform done, rv = %d\n", __FUNCTION__, rv);

		 

		if(rv == CURLE_OK)
		{
			*resp = stRespData.szMsg;
			*respSize = stRespData.iWritten;
			
			fprintf(fptr, "Data Posted Succesfully\n");

			fprintf(fptr, "%s: Response Len = [%d]\n", __FUNCTION__,
														stRespData.iWritten);

			 
		}
		else
		{
			switch(rv)
			{
			case CURLE_UNSUPPORTED_PROTOCOL: /* 1 */
				/*
				 * The URL you passed to libcurl used a protocol that this libcurl does not support.
				 * The support might be a compile-time option that you didn't use,
				 * it can be a misspelled protocol string or just a protocol libcurl has no code for.
				 */
				fprintf(fptr,   "%s: protocol not supported by library\n",
																__FUNCTION__);

				break;

			case CURLE_URL_MALFORMAT: /* 3 */
				/*
				 * The URL was not properly formatted.
				 */
				fprintf(fptr,   "%s: URL not properly formatted\n",
																__FUNCTION__);

				break;

			case CURLE_COULDNT_RESOLVE_HOST: /* 6 */
				/*
				 * Couldn't resolve host. The given remote host was not resolved.
				 */
				fprintf(fptr,   "%s: Couldnt resolve server's address\n", __FUNCTION__);

				break;

			case CURLE_COULDNT_CONNECT: /* 7 */
				/*
				 * Failed to connect() to host or proxy.
				 */
				fprintf(fptr,   "%s: Failed to connect server\n",
																__FUNCTION__);

				break;

			case CURLE_HTTP_RETURNED_ERROR: /* 22 */
				/*
				 * This is returned if CURLOPT_FAILONERROR is set TRUE
				 * and the HTTP server returns an error code that is >= 400.
				 */
				fprintf(fptr,   "%s: Got HTTP error while posting\n",
																__FUNCTION__);

				break;

			case CURLE_WRITE_ERROR: /* 23 */
				/*
				 * An error occurred when writing received data to a local file,
				 * or an error was returned to libcurl from a write callback.
				 */
				fprintf(fptr,   "%s: Failed to save the response data\n",
																__FUNCTION__);

				 

				rv = FAILURE;
				break;

			case CURLE_OUT_OF_MEMORY: /* 27 */
				/*
				 * A memory allocation request failed.
				 * This is serious badness and things are severely screwed up if this ever occurs.
				 */
				fprintf(fptr,   "%s: Facing memory shortage \n",
																__FUNCTION__);

				 

				rv = FAILURE;
				break;

			case CURLE_OPERATION_TIMEDOUT: /* 28 */
				/*
				 * Operation timeout.
				 * The specified time-out period was reached according to the conditions.
				 */
				fprintf(fptr,   "%s: Timeout happened while receiving\n",
																__FUNCTION__);

				break;

			case CURLE_SSL_CONNECT_ERROR: /* 35 */
				/*
				 * A problem occurred somewhere in the SSL/TLS handshake.
				 *
				 */
				fprintf(fptr,   "%s: SSL/TLS Handshake FAILED\n",
																__FUNCTION__);

				break;

			case CURLE_PEER_FAILED_VERIFICATION: /* 51 */
				/*
				 * The remote server's SSL certificate or SSH md5 fingerprint was deemed not OK.
				 */
				fprintf(fptr, 	"%s: Server's SSL certificate verification FAILED\n",
							__FUNCTION__);

				break;

			case CURLE_SEND_ERROR: /* 55 */
				/*
				 * Failed sending network data.
				 */
				fprintf(fptr,   "%s: Failed to post the request data\n", __FUNCTION__);

				break;

			case CURLE_RECV_ERROR: /* 56 */
				/*
				 * Failure with receiving network data.
				 */
				fprintf(fptr,   "%s: Failed to receive the response\n",
																__FUNCTION__);

				break;

			case CURLE_SSL_CACERT: /* 60 */
				/*
				 * Peer certificate cannot be authenticated with known CA certificates.
				 */
				fprintf(fptr,
								"%s: Couldnt authenticate peer certificate\n",
																__FUNCTION__);

				break;

			case CURLE_SSL_CACERT_BADFILE: /* 77 */
				/*
				 * Problem with reading the SSL CA cert (path? access rights?)
				 */
				fprintf(fptr,   "%s: Cant find SSL CA sertificate [%s]\n",
												__FUNCTION__, CA_CERT_FILE);

				break;

			default:
				fprintf(fptr,   "%s: Error [%d] occured while posting\n",
															__FUNCTION__, rv);

				break;
			}
			
	//		rv = FAILURE;
			
			/* Deallocate the allocated memory */
			if(stRespData.szMsg != NULL)
			{
				free(stRespData.szMsg);
				stRespData.szMsg = NULL;
			}

		
			fprintf(fptr,   "%s: Lib Curl Error [%s]\n",__FUNCTION__, szCurlErrBuf);
		}

		break;
	}

	fprintf(fptr, "%s: Returning [%d]\n", __FUNCTION__, rv);


	return rv;
}

/*
 * ============================================================================
 * Function Name: saveResponse
 *
 * Description	: This function performs the job of saving the XML response
 * 					coming from the server into a buffer provided as parameter.
 * 					This function is passed to the CURL library, and the
 * 					library would call this function, not us.
 *
 * Input Params	:
 * 					void * ptr -> pointer to the data that needs to be copied
 * 					size_t size-> size of one element of data
 * 					size_t cnt -> count of data members
 * 					void * des -> our buffer where the response is saved
 *
 * Output Params: totLen -> length of data saved
 * ============================================================================
 */
static size_t saveResponse(void * ptr, size_t size, size_t cnt, void * des)
{
	int				rv				= SUCCESS;
	int				iSizeLeft		= 0;
	int				totLen			= 0;
	int				iReqdLen		= 0;
	char *			cTmpPtr			= NULL;
	RESP_DATA_PTYPE	respPtr			= NULL;


	fprintf(fptr,   "%s: --- enter ---\n", __FUNCTION__);

	 

	while(1)
	{

		respPtr = (RESP_DATA_PTYPE) des;
		totLen = size * cnt;
		iSizeLeft = respPtr->iTotSize - respPtr->iWritten;

		if(totLen > iSizeLeft)
		{
			iReqdLen = (respPtr->iTotSize + totLen - iSizeLeft) + 1/* one extra buffer for NULL at the end for safety purpose*/;

			cTmpPtr = (char *) realloc(respPtr->szMsg, iReqdLen);
			if(cTmpPtr == NULL)
			{
				fprintf(fptr,   "%s: Reallocation of memory FAILED\n",
																__FUNCTION__);

				 

				rv = FAILURE;
				break;
			}

			memset(cTmpPtr + respPtr->iWritten, 0x00, iReqdLen - respPtr->iWritten);

			respPtr->iTotSize = iReqdLen;
			respPtr->szMsg = cTmpPtr;
		}

		memcpy(respPtr->szMsg + respPtr->iWritten, ptr, totLen);
		respPtr->iWritten += totLen;

		rv = totLen;

		break;
	}

	fprintf(fptr,   "%s: Returning [%d]\n", __FUNCTION__, rv);
	 


	return rv;
}

/*
 * ============================================================================
 * Function Name: initializeRespData
 *
 * Description	: This function is used for the initial setting of the response
 * 					data structure which would then be used for storing XML
 * 					response retrieved from the server being contacted.
 *
 * Input Params	: RESP_DATA_PTYPE stRespPtr -> pointer to response structure
 *
 * Output Params: SUCCESS / FAILURE
 * ============================================================================
 */
static int initializeRespData(RESP_DATA_PTYPE stRespPtr)
{
	int		rv				= SUCCESS;
	char *	cTmpPtr			= NULL;
	
#ifdef DEBUG
	char	fptr[128]	= "";
#endif

	fprintf(fptr,  "%s: --- enter ---\n", __FUNCTION__);


	cTmpPtr = (char *) malloc(4096 * sizeof(char));
	if(cTmpPtr != NULL)
	{
		/* Initialize the memory */
		memset(cTmpPtr, 0x00, 4096 * sizeof(char));
		memset(stRespPtr, 0x00, sizeof(RESP_DATA_STYPE));

		/* Assign the data */
		stRespPtr->iTotSize = 4096;
		stRespPtr->iWritten = 0;
		stRespPtr->szMsg	= cTmpPtr;
	}
	else
	{
		fprintf(fptr,  "%s: Memory allocation FAILED\n", __FUNCTION__);

		

		rv = FAILURE;
	}

	fprintf(fptr,  "%s: Returning [%d]\n", __FUNCTION__, rv);
	


	return rv;
}


/*
 * ============================================================================
 * Function Name: waitForHostConnection
 *
 * Description	:
 *
 * Input Params	:
 *
 * Output Params:
 * ============================================================================
 */
static void* waitForHostConnection(void *arg)
{
	int 		iRetVal 	          = SUCCESS;
	int			*bHostWentOffline;

	bHostWentOffline = (int*)arg;

//

/*	if(iSAFInterval == 0)
	{
		iSAFInterval = getSAFPingInterval();
		fprintf(fptr, "%s: SAF Host connection check Interval is %d", __FUNCTION__, iSAFInterval);

		lSAFIntervalMillisecs = iSAFInterval * 60 * 1000;
	}
*/
/*	fprintf(fptr, "%s: SAF Host connection check waiting time is %ld in millisecs", __FUNCTION__, lSAFIntervalMillisecs);
	*/

	while(1)
	{
		fptr2 = fopen("./flash/threadlogs.txt", "a");

		if(isHostConnected() == TRUE)
		{
			fprintf(fptr2, "%s: Host connection is available\n",__FUNCTION__);

			break;
		}
		fprintf(fptr2, "%s: Host connection is not available, checking once more...\n",__FUNCTION__);


		//Set veriable to indicate host went offline.
		*bHostWentOffline = TRUE;

		sleep(3); //Will check the connection part after the given interval of time
		fclose(fptr2);
	}

//	fprintf(fptr2, "%s: Returning [%d]\n", __FUNCTION__, iRetVal);
}

/*
 * ============================================================================
 * Function Name: isHostConnected
 *
 * Description	:
 *
 * Input Params	: none
 *
 * Output Params: TRUE / FALSE
 * ============================================================================
 */
int isHostConnected()
{
	int	bRv				= TRUE;

	fprintf(fptr2, "%s: --- enter ---\n", __FUNCTION__);


	if(SUCCESS == checkHostConn())
	{
		bRv = TRUE;
	}
	else
	{
		bRv = FALSE;
	}

	fprintf(fptr2, "%s: Returning [%s]\n", __FUNCTION__,
					(bRv == TRUE)? "TRUE" : "FALSE");


	return  bRv;
}

/*
 * ============================================================================
 * Function Name: checkHostConn
 *
 * Description	:
 *
 * Input Params	:
 *
 * Output Params: SUCCESS / FAILURE
 * ============================================================================
 */
int checkHostConn()
{
	int			rv				= SUCCESS;
	int			iCnt			= 0;
	static int	host			= PRIMARY_URL;
	CURL *		curlHandle		= NULL;
	char*		szHostUrl		= "https://sample.com";;
#ifdef DEBUG
	char		szDbgMsg[512]	= "";
#endif

	fprintf(fptr2, "%s: --- enter ---\n", __FUNCTION__);


	while(iCnt < 2)
	{
		rv = initSSICURLHandle(&curlHandle, szHostUrl);

		if(rv == SUCCESS)
		{
			/* Curl handler initialized successfully
			 * Checking connection */
			rv = chkConn(curlHandle);
			if(rv != SUCCESS)
			{
				fprintf(fptr2, "%s: FAILED to check Host connection\n",
																__FUNCTION__);
			}
			else
			{
				fprintf(fptr2, "%s: Host connection available\n", __FUNCTION__);
			}
		}

		if(rv == SUCCESS)
		{
			fprintf(fptr2, "%s: Breaking from the loop\n", __FUNCTION__);

			break;
		}

		/* If the first host is not working then try the next host */
		host ^= 1;
		iCnt++;

		fprintf(fptr2, "%s: Trying again..\n", __FUNCTION__);
	}

	fprintf(fptr2, "%s: Returning [%d]\n", __FUNCTION__, rv);


	return rv;
}

/*
 * ============================================================================
 * Function Name: chkConn
 *
 * Description	:
 *
 * Input Params	:
 *
 *
 * Output Params: SUCCESS / FAILURE
 * ============================================================================
 */
static int chkConn(CURL * curlHandle)
{
	int				rv				= SUCCESS;
	long			lCode			= 0L;
#ifdef DEBUG
	char			szDbgMsg[256]	= "";
#endif

	fprintf(fptr2, "%s: --- enter ---\n", __FUNCTION__);

	while(1)
	{
		curl_easy_setopt(curlHandle, CURLOPT_POST, 0L);
		curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
		curl_easy_setopt(curlHandle, CURLOPT_FOLLOWLOCATION, 1);

		rv = curl_easy_perform(curlHandle);
		if(rv == CURLE_OK)
		{
			fprintf(fptr2, "%s: Perform SUCCESS\n", __FUNCTION__);

		}
		else
		{
			fprintf(fptr2, "%s: Return Value from PERFORM = [%d]\n",
															__FUNCTION__, rv);

		}

		rv = curl_easy_getinfo(curlHandle, CURLINFO_RESPONSE_CODE, &lCode);

		fprintf(fptr2, "%s: Return Value RESPONSE CODE = [%d], [%ld]\n",
													__FUNCTION__, rv, lCode);


		if(rv == 0 && lCode == 200)
		{
			fprintf(fptr2, "%s: Got Successful respcode from host\n",
																__FUNCTION__);


			rv = SUCCESS;
		}
		else
		{
			fprintf(fptr2, "%s: Didnt get the good resp code from host\n"
															, __FUNCTION__);
			rv = FAILURE;
		}

		break;
	}

	if(curlHandle != NULL)
	{
		curl_easy_cleanup(curlHandle);
	}

	fprintf(fptr2, "%s: Returning [%d]\n", __FUNCTION__, rv);

	return rv;
}
-------------------------------------------------------------------
List admin: http://cool.haxx.se/list/listinfo/curl-library
Etiquette:  http://curl.haxx.se/mail/etiquette.html

Reply via email to