turnaround issue brevo emailing
This commit is contained in:
		| @@ -3,6 +3,7 @@ const path = require("path"); | |||||||
| const fs = require("fs-extra"); | const fs = require("fs-extra"); | ||||||
| const axios = require("axios"); | const axios = require("axios"); | ||||||
| const dayjs = require("dayjs"); | const dayjs = require("dayjs"); | ||||||
|  | const Mustache = require('mustache'); | ||||||
| const Checkjson = require(`./Checkjson.js`); | const Checkjson = require(`./Checkjson.js`); | ||||||
| //const smtp = require("smtp-client"); | //const smtp = require("smtp-client"); | ||||||
| const nodemailer = require("nodemailer"); | const nodemailer = require("nodemailer"); | ||||||
| @@ -212,18 +213,27 @@ Notifications.manageemail = (data, template, tribe) => { | |||||||
|    *  data optionaly can contain Cc,Bcc as array of emails and attachments as array of filename (into the server) |    *  data optionaly can contain Cc,Bcc as array of emails and attachments as array of filename (into the server) | ||||||
|    *  Await the 1st email |    *  Await the 1st email | ||||||
|    */ |    */ | ||||||
|  |   //console.log(data) | ||||||
|   if (!data.emailsto || data.emailsto.length == 0) { |   if (!data.emailsto || data.emailsto.length == 0) { | ||||||
|     return { |     return { | ||||||
|       status: 406, |       status: 406, | ||||||
|       ref: "Notifications", |       ref: "Notifications", | ||||||
|       msg: "emailsmissing", |       msg: "emailstomissing", | ||||||
|       data: data, |       data: data, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|   if (typeof data.emailsto === "string") data.emailsto = [data.emailsto]; |   if (typeof data.emailsto === "string") data.emailsto = [data.emailsto]; | ||||||
|  |   | ||||||
|  |   if (!fs.existsSync(path.resolve(template))){ | ||||||
|  |     return { | ||||||
|  |       status: 404, | ||||||
|  |       ref: "Notification", | ||||||
|  |       msg: "templatenotfound", | ||||||
|  |       data: { template:path.resolve(template) }, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   const tplemail = require(path.resolve(template)); |   const tplemail = require(path.resolve(template)); | ||||||
|   let sendit; |   let sendit={status:200,ref:"Notifications",msg:"successfullsend"}; | ||||||
|   data.emailsto.forEach(async (e, i) => { |   data.emailsto.forEach(async (e, i) => { | ||||||
|     if (Checkjson.testformat(e, "email")) { |     if (Checkjson.testformat(e, "email")) { | ||||||
|       const dat = {}; |       const dat = {}; | ||||||
| @@ -233,16 +243,17 @@ Notifications.manageemail = (data, template, tribe) => { | |||||||
|       dat.text = Mustache.render(tplemail.text, data); |       dat.text = Mustache.render(tplemail.text, data); | ||||||
|       dat.Cc = data.Cc ? tplemail.Cc.push(data.Cc) : tplemail.Cc; |       dat.Cc = data.Cc ? tplemail.Cc.push(data.Cc) : tplemail.Cc; | ||||||
|       dat.Bcc = data.Bcc ? tplemail.Bcc.push(data.Bcc) : tplemail.Bcc; |       dat.Bcc = data.Bcc ? tplemail.Bcc.push(data.Bcc) : tplemail.Bcc; | ||||||
|       dat.attachments = tplemail.attachments.push(data.attachments); |       if (data.attachments){ | ||||||
|       const sendit = Notifications.sendmail(data, tribe); |         data.attachments.forEach(a=>tplemail.attachments.push(a)) | ||||||
|  |       } | ||||||
|       if (i == 0) { |       if (i == 0) { | ||||||
|         sendit = await Notifications.sendmail(data, tribe); |         sendit = await Notifications.sendmail(dat, tribe); | ||||||
|         if (sendit.status != 200) return sendit; |         if (sendit.status != 200) return {status:406,ref:"Notifications",msg:"emailnotsent"}; | ||||||
|       } else { |       } else { | ||||||
|         Notifications.sendmail(data, tribe); |         Notifications.sendmail(dat, tribe); | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       // not a well formated email |       // not a well formated email @todo add a log file to follow it | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   return sendit; |   return sendit; | ||||||
| @@ -250,6 +261,17 @@ Notifications.manageemail = (data, template, tribe) => { | |||||||
|  |  | ||||||
| Notifications.sendmail = async (data, tribe) => { | Notifications.sendmail = async (data, tribe) => { | ||||||
|   /** |   /** | ||||||
|  |    *    * in conf global or in /itm/{tribe}.json must have valid parameter emailcontact must be authorized by the smtp | ||||||
|  |    *  "emailcontact": "noreply@smatchit.io", | ||||||
|  |    *  "smtp": { | ||||||
|  |    *    "host": "smtp-relay.brevo.com", | ||||||
|  |    *    "port": 587, | ||||||
|  |    *    "secure": false, | ||||||
|  |    *    "auth": { | ||||||
|  |    *     "user": "xx", | ||||||
|  |    *    "pass": "yy" | ||||||
|  |    *    } | ||||||
|  |    *   } | ||||||
|    * See https://nodemailer.com/message/ for available fields to add |    * See https://nodemailer.com/message/ for available fields to add | ||||||
|    * @param {string} [data.from] an email authorized by smtp used priority from header xtribe |    * @param {string} [data.from] an email authorized by smtp used priority from header xtribe | ||||||
|    * @param {string} data.to list of email separate by , |    * @param {string} data.to list of email separate by , | ||||||
| @@ -265,7 +287,7 @@ Notifications.sendmail = async (data, tribe) => { | |||||||
|    *  {filename:'img.png',path:"data:text/svg;base64.aGVsbG8gd29ybGQ="} |    *  {filename:'img.png',path:"data:text/svg;base64.aGVsbG8gd29ybGQ="} | ||||||
|    * |    * | ||||||
|    * @example data |    * @example data | ||||||
|    * {"to":"wall-ants.ndda.fr", |    * {"to":"wall-ants@ndda.fr", | ||||||
|    *  "subject":"Test", |    *  "subject":"Test", | ||||||
|    *  "html":"<h1>test welcome</h1>", |    *  "html":"<h1>test welcome</h1>", | ||||||
|    *  "text":"test welcome", |    *  "text":"test welcome", | ||||||
| @@ -305,13 +327,18 @@ Notifications.sendmail = async (data, tribe) => { | |||||||
|   const conftribfile = `../../itm/${tribe}.json`; |   const conftribfile = `../../itm/${tribe}.json`; | ||||||
|   if (fs.existsSync(conftribfile)) { |   if (fs.existsSync(conftribfile)) { | ||||||
|     const conftrib = fs.readJSONSync(conftribfile); |     const conftrib = fs.readJSONSync(conftribfile); | ||||||
|     confsmtp = conftrib.smtp; |     if (!conftrib.emailcontact){ | ||||||
|     if (!data.from) data.from = conftrib.emailcontact; |       return { | ||||||
|  |         status: 428, | ||||||
|  |         ref: "Notifications", | ||||||
|  |         msg: "missingemailcontactinconf", | ||||||
|  |         data: { tribe }, | ||||||
|  |       };   | ||||||
|  |     } | ||||||
|  |     confsmtp = conftrib.smtp; | ||||||
|  |     if (!data.from || data.from == conf.emailcontact) data.from = conftrib.emailcontact; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   //const client = smtp.connect(confsmtp); |  | ||||||
|   const transporter = await nodemailer.createTransport(confsmtp); |   const transporter = await nodemailer.createTransport(confsmtp); | ||||||
|   //@todo add attachments management |  | ||||||
|   if (data.filelist) { |   if (data.filelist) { | ||||||
|     data.attachments = []; |     data.attachments = []; | ||||||
|     let missingfile = []; |     let missingfile = []; | ||||||
| @@ -331,6 +358,7 @@ Notifications.sendmail = async (data, tribe) => { | |||||||
|   } |   } | ||||||
|   //console.log("data:", data); |   //console.log("data:", data); | ||||||
|   const res = await transporter.sendMail(data); |   const res = await transporter.sendMail(data); | ||||||
|  |   //console.log(res) | ||||||
|   if ( |   if ( | ||||||
|     res.accepted && |     res.accepted && | ||||||
|     data.to.split(",").reduce((acc, m) => acc && res.accepted.includes(m), true) |     data.to.split(",").reduce((acc, m) => acc && res.accepted.includes(m), true) | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								models/PagansPeter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								models/PagansPeter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | |||||||
|  | const glob = require("glob"); | ||||||
|  | const path = require("path"); | ||||||
|  | const dayjs = require("dayjs"); | ||||||
|  | const fs = require("fs-extra"); | ||||||
|  | const axios = require("axios"); | ||||||
|  | const Mustache = require("mustache"); | ||||||
|  | const openpgp = require("openpgp"); | ||||||
|  | const Notifications = require("./Notifications.js"); | ||||||
|  | const Odmdb = require("./Odmdb.js"); | ||||||
|  |  | ||||||
|  | const conf = require(`../../../conf.json`); | ||||||
|  | const currentmod = "Pagans"; | ||||||
|  | const log = conf.api.activelog.includes(currentmod); | ||||||
|  | /** | ||||||
|  |  * Pagan Management numeric Identity and Person  (Person = Pagan Id + tribe) | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | const Pagans = {}; | ||||||
|  |  | ||||||
|  | Pagans.create=()=>{ | ||||||
|  | //from router.post("/", checkHeaders, isAuthenticated, async (req, res) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Pagans.joinpersontotribe=()=>{ | ||||||
|  | // from router.post("/", checkHeaders, isAuthenticated, async (req, res) | ||||||
|  |  | ||||||
|  | } | ||||||
|  | /** | ||||||
|  |  *  Remove authentification token after a logout | ||||||
|  |  * @param {string} alias | ||||||
|  |  * @param {string} tribe | ||||||
|  |  * @param {integer} xdays | ||||||
|  |  * @param {string} xhash | ||||||
|  |  * @returns {status:200, ref:"Pagans",msg:"logout"} | ||||||
|  |  * tmpfs name file has to be on line with the tmpfs create by isAuthenticated | ||||||
|  |  *  tmpfs contain profils name for a tribe/ | ||||||
|  |  */ | ||||||
|  | Pagans.logout = (alias, tribe, xdays, xhash) => { | ||||||
|  |   //console.log(alias, tribe, xdays, xhash); | ||||||
|  |   // inline with middleware isAuthenticated.js | ||||||
|  |   let tmpfs = `../../tmp/tokens/${alias}_${tribe}_${xdays}`; | ||||||
|  |   //max filename in ext4: 255 characters | ||||||
|  |   tmpfs += `_${xhash.substring(150, 150 + tmpfs.length - 249)}.json`; | ||||||
|  |   fs.remove(tmpfs); | ||||||
|  |   if (log) console.log(currentmod, "logout token", tmpfs); | ||||||
|  |   return { status: 200, ref: "Pagans", msg: "logout" }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {string} alias a alias that exist or not | ||||||
|  |  * @return {object}  { status: 200, ref:"pagans",msg:"aliasexist",data: { alias, publicKey } } | ||||||
|  |  *                   { status: 404, ref:"pagans",msg:"aliasdoesnotexist",data: { alias} } | ||||||
|  |  * | ||||||
|  |  **/ | ||||||
|  | Pagans.getalias = (alias) => { | ||||||
|  |   //bypass Odmdb cause all is public save ressources | ||||||
|  |   console.log(path.resolve(`../objects/pagans/itm/${alias}.json`)); | ||||||
|  |   if (fs.existsSync(`../objects/pagans/itm/${alias}.json`)) { | ||||||
|  |     return { | ||||||
|  |       status: 200, | ||||||
|  |       ref: "Pagans", | ||||||
|  |       msg: "aliasexist", | ||||||
|  |       data: fs.readJSONSync(`../objects/pagans/itm/${alias}.json`), | ||||||
|  |     }; | ||||||
|  |   } else { | ||||||
|  |     return { | ||||||
|  |       status: 404, | ||||||
|  |       ref: "Pagans", | ||||||
|  |       msg: "aliasdoesnotexist", | ||||||
|  |       data: { alias }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Send email with alias's keys to email or person alias person.recovery.email | ||||||
|  |  * | ||||||
|  |  * If email or pubkey is undefined then get data from tribe/person(alias) | ||||||
|  |  * Send email with keys | ||||||
|  |  * @param {object} data | ||||||
|  |  * @param {string} data.alias | ||||||
|  |  * @param {pgpPrivate} [data.privatekey] | ||||||
|  |  * @param {string} [data.passphrase] | ||||||
|  |  * @param {string} data.tribe | ||||||
|  |  * @param {pgpPublic} [data.publickey] | ||||||
|  |  * @param {string} [data.email] | ||||||
|  |  * @param {string} data.lg | ||||||
|  |  */ | ||||||
|  | Pagans.sendmailkey = (data) => { | ||||||
|  |   if (log) | ||||||
|  |     console.log( | ||||||
|  |       currentmod, | ||||||
|  |       data.alias, | ||||||
|  |       "-", | ||||||
|  |       data.privatekey ? data.privatekey.substring(0, 10) : "", | ||||||
|  |       "-", | ||||||
|  |       data.tribe, | ||||||
|  |       "-", | ||||||
|  |       data.passphrase, | ||||||
|  |       "-", | ||||||
|  |       data.publickey ? data.publickey.substring(0, 10) : "", | ||||||
|  |       "-", | ||||||
|  |       data.email, | ||||||
|  |       "-", | ||||||
|  |       data.lg | ||||||
|  |     ); | ||||||
|  |   const person = { | ||||||
|  |     alias: data.alias, | ||||||
|  |     privatekey: data.privatekey, | ||||||
|  |     tribe: data.tribe, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if (!data.publickey || !data.email || !data.privatekey) { | ||||||
|  |     const personfile = `../../${data.tribe}/objects/persons/itm/${data.alias}.json`; | ||||||
|  |     if (!fs.existsSync(personfile)) { | ||||||
|  |       return { | ||||||
|  |         status: 404, | ||||||
|  |         ref: "Pagans", | ||||||
|  |         msg: "persondoesnotexist", | ||||||
|  |         data: { alias: data.alias, tribe: data.tribe }, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     const persondata = fs.readJsonSync(personfile); | ||||||
|  |     if (!persondata.recoveryauth) { | ||||||
|  |       return { | ||||||
|  |         status: 404, | ||||||
|  |         ref: "Pagans", | ||||||
|  |         msg: "personhasnorecoveryauth", | ||||||
|  |         data: { alias: data.alias, tribe: data.tribe, email: data.email }, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     person.email = persondata.recoveryauth.email; | ||||||
|  |     person.publickey = persondata.recoveryauth.publickey; | ||||||
|  |     person.privatekey = persondata.recoveryauth.privatekey; | ||||||
|  |     person.passphrase = persondata.recoveryauth.passphrase; | ||||||
|  |   } else { | ||||||
|  |     person.email = data.email; | ||||||
|  |     person.passphrase = data.passphrase; | ||||||
|  |     person.publickey = data.publickey; | ||||||
|  |   } | ||||||
|  |   person.avecpassphrase = person.passphrase != ""; | ||||||
|  |   let tplfile = `../../${data.tribe}/template/createidentity_${data.lg}.js`; | ||||||
|  |   if (!fs.existsSync(tplfile)) { | ||||||
|  |     tplfile = `../template/createidentity_${data.lg}.js`; | ||||||
|  |     if (!fs.existsSync(tplfile)) { | ||||||
|  |       return { | ||||||
|  |         status: 406, | ||||||
|  |         ref: "Pagans", | ||||||
|  |         msg: "templatedoesnotexist", | ||||||
|  |         data: { tplfile }, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   const tplemail = require(path.resolve(tplfile)); | ||||||
|  |   /* | ||||||
|  |   Remove from attachments for less user confusing | ||||||
|  |      { | ||||||
|  |         filename:`${person.alias}_publickey.txt`, | ||||||
|  |         content: person.publickey, | ||||||
|  |         contentType:"text/plain" | ||||||
|  |       }, | ||||||
|  |   */ | ||||||
|  |   const maildata = { | ||||||
|  |     from: tplemail.sender, | ||||||
|  |     to: person.email, | ||||||
|  |     subject: Mustache.render(tplemail.subject, person), | ||||||
|  |     html: Mustache.render(tplemail.html, person), | ||||||
|  |     text: Mustache.render(tplemail.text, person), | ||||||
|  |     attachments: [ | ||||||
|  |       { | ||||||
|  |         filename: `${person.alias}_privatekey.txt`, | ||||||
|  |         content: person.privatekey, | ||||||
|  |         contentType: "text/plain", | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }; | ||||||
|  |   return Notifications.sendmail(maildata, data.tribe); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Pagans.authenticatedetachedSignature = async ( | ||||||
|  |   alias, | ||||||
|  |   pubK, | ||||||
|  |   detachedSignature, | ||||||
|  |   message | ||||||
|  | ) => { | ||||||
|  |   /** | ||||||
|  |    *  Check that a message was signed with a privateKey from a publicKey | ||||||
|  |    *  This is not necessary if isAuthenticated, but can be usefull to double check | ||||||
|  |    *  @TODO finish it and implement it also in  /apxpagan.js for browser | ||||||
|  |    *  @alias {string} alias link to the publicKey | ||||||
|  |    *  @pubK {string} publiKey text format | ||||||
|  |    *  @detachedSignature  {string} a detachedsignatured get from apx.detachedSignature | ||||||
|  |    *  @message {string} the message signed | ||||||
|  |    *  @return {boolean} true the message was signed by alias | ||||||
|  |    *                    false the message was not signed by alias | ||||||
|  |    */ | ||||||
|  |   const publicKey = await openpgp.readKey({ armoredKey: pubK }); | ||||||
|  |   const msg = await openpgp.createMessage({ text: message }); | ||||||
|  |   const signature = await openpgp.readSignature({ | ||||||
|  |     armoredSignature: detachedSignature, // parse detached signature | ||||||
|  |   }); | ||||||
|  |   const verificationResult = await openpgp.verify({ | ||||||
|  |     msg, // Message object | ||||||
|  |     signature, | ||||||
|  |     verificationKeys: publicKey, | ||||||
|  |   }); | ||||||
|  |   const { verified, keyID } = verificationResult.signatures[0]; | ||||||
|  |   try { | ||||||
|  |     await verified; // throws on invalid signature | ||||||
|  |     if (log) console.log(currentmod, "Signed by key id " + keyID.toHex()); | ||||||
|  |     return KeyId.toHex().alias == alias; | ||||||
|  |   } catch (e) { | ||||||
|  |     if (log) | ||||||
|  |       console.log(currentmod, "Signature could not be verified: " + e.message); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = Pagans; | ||||||
| @@ -44,13 +44,24 @@ router.get("/messages/:alias/:tribeId", (req, res) => { | |||||||
| /** | /** | ||||||
|  * @api {POST} adminapi/notifications/sendmail/:tribe/:template -Send personnalize emails |  * @api {POST} adminapi/notifications/sendmail/:tribe/:template -Send personnalize emails | ||||||
|  * @apiName Sendmail |  * @apiName Sendmail | ||||||
|  * @apiDescription Send personnalize email with data from template store in ../../{tribe}/template/{template}.json |  * @apiDescription Send personnalize email with data from template store in ../../{tribe}/template/{template}.json and smtp in conf global or in /itm/{tribe}.json that must have valid parameter emailcontact must be authorized by the smtp | ||||||
|  |    *  "emailcontact": "noreply@smatchit.io", | ||||||
|  |    *  "smtp": { | ||||||
|  |    *    "host": "smtp-relay.brevo.com", | ||||||
|  |    *    "port": 587, | ||||||
|  |    *    "secure": false, | ||||||
|  |    *    "auth": { | ||||||
|  |    *     "user": "xx", | ||||||
|  |    *    "pass": "yy" | ||||||
|  |    *    } | ||||||
|  |    *   } | ||||||
|  |  | ||||||
|  * @apiGroup Notifications |  * @apiGroup Notifications | ||||||
|  * |  * | ||||||
|  * @apiParam {string} template |  * @apiParam {string} template | ||||||
|  * @apiParam {string} tribe |  * @apiParam {string} tribe | ||||||
|  * @apiBody {array} emails to send (array of valid email) |  * @apiBody {array} emails to send (array of valid email) | ||||||
|  * @apiBody {object} Data to personnalize template |  * @apiBody {object} data to personnalize template | ||||||
|  * |  * | ||||||
|  * @apiSuccess {object}  notif content |  * @apiSuccess {object}  notif content | ||||||
|  * @apiSuccessExample {json} Success-Response: |  * @apiSuccessExample {json} Success-Response: | ||||||
| @@ -59,24 +70,19 @@ router.get("/messages/:alias/:tribeId", (req, res) => { | |||||||
|  * bouture |  * bouture | ||||||
|  */ |  */ | ||||||
| router.post( | router.post( | ||||||
|   "/sendmail/:tribe/:template/:await", |   "/sendmail/:tribe/:template", | ||||||
|   checkHeaders, |   checkHeaders, | ||||||
|   isAuthenticated, |   isAuthenticated, | ||||||
|   (req, res) => { |   async (req, res) => { | ||||||
|     const pathtpl = `../../${req.params.tribe}/${req.params.template}.js`; |     const data = req.body.data; | ||||||
|     if (fs.existsSync(pathtpl)) { |     data.emailsto = req.body.emails; | ||||||
|       const sendemail = Notification.manageemail(req.body, pathtpl,req.params.tribe); |     const pathtpl = `../../${req.params.tribe}/template/${req.params.template}_${req.session.header.xlang}.js`; | ||||||
|  |     const sendemail = await Notifications.manageemail( | ||||||
|  |       data, | ||||||
|  |       pathtpl, | ||||||
|  |       req.params.tribe | ||||||
|  |     ); | ||||||
|     res.status(sendemail.status).send(sendemail); |     res.status(sendemail.status).send(sendemail); | ||||||
|     } else { |  | ||||||
|       res |  | ||||||
|         .status(404) |  | ||||||
|         .send({ |  | ||||||
|           status: 404, |  | ||||||
|           ref: "Notification", |  | ||||||
|           msg: "templatenotfound", |  | ||||||
|           data: { pathtpl }, |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
| @@ -99,9 +105,7 @@ router.post( | |||||||
| router.post("/registeranonymous", checkHeaders, (req, res) => { | router.post("/registeranonymous", checkHeaders, (req, res) => { | ||||||
|   //console.log("list registration ",req.body) |   //console.log("list registration ",req.body) | ||||||
|   if (!req.body.typekey || !["email", "telephone"].includes(req.body.typekey)) { |   if (!req.body.typekey || !["email", "telephone"].includes(req.body.typekey)) { | ||||||
|     return res |     return res.status(406).json({ | ||||||
|       .status(406) |  | ||||||
|       .json({ |  | ||||||
|       status: 406, |       status: 406, | ||||||
|       ref: "Notifications", |       ref: "Notifications", | ||||||
|       msg: "typekeyunknown", |       msg: "typekeyunknown", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user