راستی آزمایی ایمیل (Email Confirmation) با React – راهنمای کاربردی


در این نوشته به بررسی روش تأیید ایمیل یا همان راستی آزمایی ایمیل در React میپردازیم.
روش تأیید ایمیل
شاید تأیید ایمیل و راستی آزمایی آن موضوع سادهای به نظر برسد. تأیید ایمیل دستکم از زمانی که خود ایمیل متولد شده، وجود داشته است و ما گردش کاری آن را به خوبی میدانیم. یک ایمیل را برای تأیید از کاربر دریافت میکنیم و یک لینک به آن آدرس ارسال میکنیم. سپس پایگاه داده را بهروزرسانی میکنیم تا زمانی که کاربر روی لینک کلیک کرد، ایمیل وی را تأیید کند. همان طور که میبینید همه کار در سه گام ساده اجرا میشود. اما برای این که بتوان این کار را انجام داد به یک اپلیکیشن کامل نیاز داریم و از این رو فرصت مناسبی برای یادگیری طرز کار Mongo ،Express ،React و Node و همکاری آنها با هم به نظر میآید.
کلاینت React
کلاینت خود را به وسیله Create React App (+) بوتاسترپ میکنیم. ما فرض میکنیم که شما از قبل تجربیاتی در این زمینه دارید.
برای این که بتوانید مطالب مطرح شده در این راهنما را دنبال کنید تنها به سه چیز نیاز دارید:
- یک اپلیکیشن که پیش از آن که بتواند کاری انجام دهد در وضعیت قابل کارکرد باشد.
- بازخورد یعنی چیزی که وقتی اپلیکیشن کاری انجام میدهد اتفاق میافتد.
- تأییدیه که نشان میدهد اپلیکیشن کار خود را به پایان برده است.
کلاینت ما برای این اپلیکیشن تنها چند کامپوننت دارد و میتواند با کد و کامنت بسیار کمی که در ادامه نشان میدهیم نوشته شود. در این آموزش تلاش نکردهایم از حجم کد بکاهیم تا تجربه کاربری بهتری داشته باشیم.
نکته: شما میتوانید از Bit (+) برای مدیریت کامپوننتهای با قابلیت استفاده مجدد بهره بگیرید و در زمان خود صرفهجویی کنید. مجموعهای از کامپوننتهای مفید را نگهداری کنید و از آنها در پروژههای مختلف استفاده کرده و به سادگی تغییرات را همگامسازی کنید. Bit به ساخت سریعتر اپلیکیشنها کمک زیادی میکند.
فایل App.js
اینک نوبت به کدنویسی در زبان جاوا اسکریپت رسیده است. فایل App.js اپلیکیشن ما به صورت زیر خواهد بود:
1import React, { Component } from 'react'
2import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom'
3import Notifications from 'react-notify-toast'
4import 'react-toastify/dist/ReactToastify.css'
5
6import Landing from './components/Landing'
7import Confirm from './components/Confirm'
8import Spinner from './components/Spinner'
9import Footer from './components/Footer/Footer'
10import { API_URL } from './config'
11import './App.css'
12
13export default class App extends Component {
14
15 // A bit of state to make sure the server is up and running before the user
16 // can interact with the app.
17 state = {
18 loading: true
19 }
20
21 // When the component mounts, a simple GET request is made to 'wake up' the
22 // server. A lot of free services like Heroku and Now.sh will put your server
23 // to sleep if no one has used your application in a few minutes. Using a
24 // service like uptimerobot.com to ping the server regularly can mitigate
25 // sleepiness.
26 componentDidMount = () => {
27 fetch(`${API_URL}/wake-up`)
28 .then(res => res.json())
29 .then(() => {
30 this.setState({ loading: false })
31 })
32 .catch(err => console.log(err))
33 }
34
35 // You are probaly used to seeing React 'render()' methods written like this:
36 //
37 // render() {
38 // return (
39 // <Some jsx />
40 // )
41 // }
42 //
43 // Below is a version of writing a 'render()' that also works. The 'why does
44 // it work?' is related to the 'this' keyword in JavaScript and is beyond the
45 // scope of this post.
46
47 render = () => {
48
49 // The 'content' function determines what to show the user based on whether
50 // the server is awake or not.
51 const content = () => {
52
53 // The server is still asleep, so provide a visual cue with the <Spinner />
54 // component to give the user that feedback.
55 if (this.state.loading) {
56 return <Spinner size='8x' spinning='spinning' />
57 }
58
59 // The server is awake! React Router is used to either show the
60 // <Landing /> component where the emails are collected or the <Confirm />
61 // component where the emails are confirmed.
62 return (
63 <BrowserRouter>
64 <Switch>
65 {/*
66 the ':id' in this route will be the unique id the database
67 creates and is available on 'this.props' inside the <Confirm />
68 component at this.props.match.params.id
69 */}
70 <Route exact path='/confirm/:id' component={Confirm} />
71 <Route exact path='/' component={Landing} />
72 <Redirect from='*' to='/'/>
73 </Switch>
74 </BrowserRouter>
75 )
76 }
77
78 return (
79 // The 'container' class uses flexbox to position and center its three
80 // children: <Notifications />, <main> and <Footer />
81 <div className='container fadein'>
82 {/*
83 <Notifications > component from 'react-notify-toast' This is the
84 placeholder on the dom that will hold all the feedback toast messages
85 whenever notify.show('My Message!') is called.
86 */}
87 <Notifications />
88 <main>
89 {content()}
90 </main>
91 {/*
92 For every Medium post I write I include a demo app that uses the same
93 footer. So, I have abstracted that out to use on future posts with
94 just a couple of props passed in.
95 */}
96 <Footer
97 mediumId={'257e5d9de725'}
98 githubRepo={'react-confirm-email'}
99 />
100 </div>
101 )
102 }
103}
فایل Landing.js
این فایل شامل کامپوننتی است که وقتی کاربر وارد اپلیکیشن میشود نمایش مییابد.
1import React, { Component } from 'react'
2import { notify } from 'react-notify-toast'
3import Spinner from './Spinner'
4import { API_URL } from '../config'
5
6export default class Landing extends Component {
7
8 // A bit of state to give the user feedback while their email address is being
9 // added to the User model on the server.
10 state = {
11 sendingEmail: false
12 }
13
14 onSubmit = event => {
15 event.preventDefault()
16 this.setState({ sendingEmail: true})
17
18 // Super interesting to me that you can mess with the upper and lower case
19 // of the headers on the fetch call and the world does not explode.
20 fetch(`${API_URL}/email`, {
21 method: 'pOSt',
22 headers: {
23 aCcePt: 'aPpliCaTIon/JsOn',
24 'cOntENt-type': 'applicAtion/JSoN'
25 },
26 body: JSON.stringify({ email: this.email.value })
27 })
28 .then(res => res.json())
29 .then(data => {
30
31 // Everything has come back successfully, time to update the state to
32 // reenable the button and stop the <Spinner>. Also, show a toast with a
33 // message from the server to give the user feedback and reset the form
34 // so the user can start over if she chooses.
35 this.setState({ sendingEmail: false})
36 notify.show(data.msg)
37 this.form.reset()
38 })
39 .catch(err => console.log(err))
40 }
41
42 render = () => {
43
44 // This bit of state provides user feedback in the component when something
45 // changes. sendingEmail is flipped just before the fetch request is sent in
46 // onSubmit and then flipped back when data has been received from the server.
47 // How many times is the 'sendingEmail' variable used below?
48 const { sendingEmail } = this.state
49
50 return (
51 // A ref is put on the form so that it can be reset once the submission
52 // process is complete.
53 <form
54 onSubmit={this.onSubmit}
55 ref={form => this.form = form}
56 >
57 <div>
58 <input
59 type='email'
60 name='email'
61 ref={input => this.email = input}
62 required
63 />
64 {/*
65 Putting the label after the input allows for that neat transition
66 effect on the label when the input is focused.
67 */}
68 <label htmlFor='email'>email</label>
69 </div>
70 <div>
71 {/*
72 While the email is being sent from the server, provide feedback that
73 something is happening by disabling the button and showing a
74 <Spinner /> inside the button with a smaller 'size' prop passed in.
75 */}
76 <button type='submit' className='btn' disabled={sendingEmail}>
77 {sendingEmail
78 ? <Spinner size='lg' spinning='spinning' />
79 : "Let's Go!"
80 }
81 </button>
82 </div>
83 </form>
84 )
85 }
86}
فایل Confirm.js
هنگامی که کاربر روی لینک یکتای موجود در ایمیلی که به آدرسش ارسال کردیم کلیک کند، این کامپوننت از سوی React Router (+) بارگذاری میشود.
1import React, {Component} from 'react'
2import { Link } from 'react-router-dom'
3import { notify } from 'react-notify-toast'
4import Spinner from './Spinner'
5import { API_URL } from '../config'
6
7export default class Confirm extends Component {
8
9 // A bit of state to give the user feedback while their email
10 // address is being confirmed on the User model on the server.
11 state = {
12 confirming: true
13 }
14
15 // When the component mounts the mongo id for the user is pulled from the
16 // params in React Router. This id is then sent to the server to confirm that
17 // the user has clicked on the link in the email. The link in the email will
18 // look something like this:
19 //
20 // http://localhost:3000/confirm/5c40d7607d259400989a9d42
21 //
22 // where 5c40d...a9d42 is the unique id created by Mongo
23 componentDidMount = () => {
24 const { id } = this.props.match.params
25
26 fetch(`${API_URL}/email/confirm/${id}`)
27 .then(res => res.json())
28 .then(data => {
29 this.setState({ confirming: false })
30 notify.show(data.msg)
31 })
32 .catch(err => console.log(err))
33 }
34
35 // While the email address is being confirmed on the server a spinner is
36 // shown that gives visual feedback. Once the email has been confirmed the
37 // spinner is stopped and turned into a button that takes the user back to the
38 // <Landing > component so they can confirm another email address.
39 render = () =>
40 <div className='confirm'>
41 {this.state.confirming
42 ? <Spinner size='8x' spinning={'spinning'} />
43 : <Link to='/'>
44 <Spinner size='8x' spinning={''} />
45 </Link>
46 }
47 </div>
48}
فایل Spinner.js
اگر به کدهایی که در بخش قبلی مطرح کردیم، به قدر کافی دقت کرده باشید، متوجه شدهاید که ما در چهار موقعیت مختلف از کامپوننت < / Spinner> استفاده کردهایم. تنها چیزی که در این دفعات تغییر یافته است props است که به کامپوننت ارسال میشود.
Spinner.js بازخورد را در موقعیتهای زیر ارائه میکند:
- هنگامی که اپلیکیشن بارگذاری میشود.
- درون دکمه، هنگامی که فشرده میشود.
- هنگامی که لینک ایمیل کلیک میشود و سرور تأیید میکند.
- زمانی که دکمه ریاستارت کردن کل فرایند، فشرده میشود.
همه این موارد تنها در 8 خط از کد React نوشته شدهاند.
1import React from 'react'
2import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3import { faSync } from '@fortawesome/free-solid-svg-icons'
4
5export default props =>
6 <div className={`fadeIn ${props.spinning}`}>
7 <FontAwesomeIcon icon={faSync} size={props.size} />
8 </div>
فایل Spinner.js انتهای کد کلاینت است. البته متوجه هستیم که این همه کد برای یک وظیفه ساده برای تأیید آدرس ایمیل زیاد به نظر میرسد. در واقع اگر بخش بازخورد و کامنت ها را حذف کنیم، تقریباً نیمی از حجم این کد کاسته میشود؛ اما در این حالت تجربه کاربری نیز دقیقاً نصف میشود!
نکته مهم این است که کاربر را در جریان کارهایی که در حال اجرا هستند قرار دهیم و به علاوه اپلیکیشنی داشته باشیم که پیش از آن که کاربر بخواهد کاری انجام دهد، کاملاً بارگذاری شده و آماده اجرا باشد.
گرچه ارائه بازخورد موجب میشود که حجم کد افزایش یابد، اما از سوی دیگر موجب افزایش کیفیت تجربه کاربری میشود. اینک باید کاری کنیم که کلاینت و سرور بتوانند با هم صحبت کنند.
نوشتن کد سرور
در این بخش کدهای مورد نیاز در سمت سرور را مینویسیم.
سرور
جهت راهاندازی و اجرا کردن سرور باید چندین مرحله از تنظیمات را طی کنیم:
- یک آدرس ایمیل ایجاد کند تا برای این اپلیکیشن مورد استفاده قرار دهید.
- Mongo را اجرا کنید تا مکانی برای ذخیره آدرسهای ایمیل داشته باشیم.
دریافت یک آدرس Gmail جدید
شما میتواند از این لینک (+) برای ثبت یک حساب کاربری جدید جیمیل اقدام کنید. دقت کنید که نباید از یک حساب جیمیل که برایتان مهم است استفاده کنید، چون اطلاعات رمز عبور آن در یک فایل env. روی نقاط مختلف سیستم ذخیره خواهد شد و علاوه بر آن چنان که در ادامه اشاره خواهیم کرد، امکان اجرای اپلیکیشنهای با امنیت کمتر (allow less secure apps) را در آن فعال خواهیم کرد. از این رو پیشگیری بهتر از درمان است و یک حساب کاربری جیمیل جدید میسازیم.
شما میتوانید این اپلیکیشن را با استفاده از آدرس ایمیلی از هر ارائه دهنده سرویس ایمیل مورد استفاده قرار دهید. برخی تنظیمات ممکن است در فایل sendEmail.js که در ادامه آمده است، اندکی متفاوت باشد و این تنظیمات به مشخصات ارائه دهنده ایمیل وابسته است. ما به منظور حفظ سادگی فرایند، در این راهنما از Gmail استفاده میکنیم.
پس از این که حساب جیمیل خود را ساختید، باید اطلاعات احراز هویت برای این حساب کاربری جدید را در یک فایل env. روی سرور قرار دهید. ما از فایل server/.env استفاده میکنیم و محتوای آن چنین خواهد بود:
MAIL_USER=your_new_email_address@gmail.com MAIL_PASS=your_new_password
نکته مهم: برای این که حساب جیمیل جدیدی که ساختهاید بتواند به نیابت از شما ایمیلهایی ارسال کند و بدین ترتیب اپلیکیشن ما بتواند کار کند باید امکان «Less secure app access» را فعال کنیم. مراحل این کار در این صفحه پشتیبانی گوگل (+) توضیح داده شده است.
اجرا کردن Mongo
اگر از قبل Mongo را به صورت محلی نصب کرده باشید، اینک میدانید که باید از دستور زیر استفاده کنید:
$ mongod
فایل user.model.js
در مدل Mongoose ما تنها دو چیز را ردگیری میکنیم. یک آدرس ایمیل و این که آیا تأیید شده است یا نه.
1const mongoose = require('mongoose')
2const Schema = mongoose.Schema
3
4// Data we need to collect/confirm to have the app go.
5const fields = {
6 email: {
7 type: String
8 },
9 confirmed: {
10 type: Boolean,
11 default: false
12 }
13}
14
15// One nice, clean line to create the Schema.
16const userSchema = new Schema(fields)
17
18module.exports = mongoose.model('User', userSchema)
فایل server.js
سعی میکنیم که این فایل را تا حد ممکن سبک نگه داریم و صرفاً به عنوان یک پوسته برای فایلهای دیگر کار کند. بنابراین تنها چند «میانافزار» (middleware) در آن قرار میدهیم و اتصالی به پایگاه داده ایجاد میکنیم.
1require('dotenv').config()
2const express = require('express')
3const mongoose = require('mongoose')
4const cors = require('cors')
5
6const app = express()
7const emailController = require('./email/email.controller')
8const { PORT, CLIENT_ORIGIN, DB_URL } = require('./config')
9
10// Only allow requests from our client
11app.use(cors({
12 origin: CLIENT_ORIGIN
13}))
14
15// Allow the app to accept JSON on req.body
16app.use(express.json())
17
18// This endpoint is pinged every 5 mins by uptimerobot.com to prevent
19// free services like Heroku and Now.sh from letting the app go to sleep.
20// This endpoint is also pinged every time the client starts in the
21// componentDidMount of App.js. Once the app is confirmed to be up, we allow
22// the user to perform actions on the client.
23app.get('/wake-up', (req, res) => res.json('?'))
24
25// This is the endpoint that is hit from the onSubmit handler in Landing.js
26// The callback is shelled off to a controller file to keep this file light.
27app.post('/email', emailController.collectEmail)
28
29// Same as above, but this is the endpoint pinged in the componentDidMount of
30// Confirm.js on the client.
31app.get('/email/confirm/:id', emailController.confirmEmail)
32
33// Catch all to handle all other requests that come into the app.
34app.use('*', (req, res) => {
35 res.status(404).json({ msg: 'Not Found' })
36})
37
38// To get rid of all those semi-annoying Mongoose deprecation warnings.
39const options = {
40 useCreateIndex: true,
41 useNewUrlParser: true,
42 useFindAndModify: false
43}
44
45// Connecting the database and then starting the app.
46mongoose.connect(DB_URL, options, () => {
47 app.listen(PORT, () => console.log('?'))
48})
49// The most likely reason connecting the database would error out is because
50// Mongo has not been started in a separate terminal.
51.catch(err => console.log(err))
فایل email.controller.js
در نهایت نوبت به مغز پردازشی عملیات میرسد. فایل email.controller.js اطلاعات مختلف مربوطه را از مسیرهای گوناگون گردآوری میکند، پایگاه داده را بر مبنای این اطلاعات بهروزرسانی کرده و بازخورد را به کلاینت بازمیگرداند، به طوری که کاربر متوجه میشود چه اتفاقی افتاده است.
نکته مهم که باید در مورد این فایل توجه داشته باشیم این است که دادههای ارائه شده یعنی محتواهایی که در ایمیل ارسال میشوند یا پیامهایی که در پاسخ دریافت میشوند هر کدام به فایلهای مربوطه خود میروند. بدین ترتیب کنترلر ما تمیز و سریع باقی میماند.
1const User = require('../user.model')
2const sendEmail = require('./email.send')
3const msgs = require('./email.msgs')
4const templates = require('./email.templates')
5
6// The callback that is invoked when the user submits the form on the client.
7exports.collectEmail = (req, res) => {
8 const { email } = req.body
9
10 User.findOne({ email })
11 .then(user => {
12
13 // We have a new user! Send them a confirmation email.
14 if (!user) {
15 User.create({ email })
16 .then(newUser => sendEmail(newUser.email, templates.confirm(newUser._id)))
17 .then(() => res.json({ msg: msgs.confirm }))
18 .catch(err => console.log(err))
19 }
20
21 // We have already seen this email address. But the user has not
22 // clicked on the confirmation link. Send another confirmation email.
23 else if (user && !user.confirmed) {
24 sendEmail(user.email, templates.confirm(user._id))
25 .then(() => res.json({ msg: msgs.resend }))
26 }
27
28 // The user has already confirmed this email address
29 else {
30 res.json({ msg: msgs.alreadyConfirmed })
31 }
32
33 })
34 .catch(err => console.log(err))
35}
36
37// The callback that is invoked when the user visits the confirmation
38// url on the client and a fetch request is sent in componentDidMount.
39exports.confirmEmail = (req, res) => {
40 const { id } = req.params
41
42 User.findById(id)
43 .then(user => {
44
45 // A user with that id does not exist in the DB. Perhaps some tricky
46 // user tried to go to a different url than the one provided in the
47 // confirmation email.
48 if (!user) {
49 res.json({ msg: msgs.couldNotFind })
50 }
51
52 // The user exists but has not been confirmed. We need to confirm this
53 // user and let them know their email address has been confirmed.
54 else if (user && !user.confirmed) {
55 User.findByIdAndUpdate(id, { confirmed: true })
56 .then(() => res.json({ msg: msgs.confirmed }))
57 .catch(err => console.log(err))
58 }
59
60 // The user has already confirmed this email address.
61 else {
62 res.json({ msg: msgs.alreadyConfirmed })
63 }
64
65 })
66 .catch(err => console.log(err))
67}
فایل email.msgs.js
لازم است که کدهای پوسته غیر کارکردی مانند این را در فایلهای خاص خود قرار دهیم. بدین ترتیب فایلهای دارای کارکردهای معین، منسجم میمانند و استدلال در مورد منطق اپلیکیشن آسانتر میشود.
1module.exports = {
2 confirm: 'Email sent, please check your inbox to confirm',
3 confirmed: 'Your email is confirmed!',
4 resend: 'Confirmation email resent, maybe check your spam?',
5 couldNotFind: 'Could not find you!',
6 alreadyConfirmed: 'Your email was already confirmed'
7}
فایل email.send.js
Nodemailer (+) یک ماژول npm است که ایمیل را ارسال میکند.
1const nodemailer = require('nodemailer')
2
3// The credentials for the email account you want to send mail from.
4const credentials = {
5 host: 'smtp.gmail.com',
6 port: 465,
7 secure: true,
8 auth: {
9 // These environment variables will be pulled from the .env file
10 user: process.env.MAIL_USER,
11 pass: process.env.MAIL_PASS
12 }
13}
14
15// Getting Nodemailer all setup with the credentials for when the 'sendEmail()'
16// function is called.
17const transporter = nodemailer.createTransport(credentials)
18
19// exporting an 'async' function here allows 'await' to be used
20// as the return value of this function.
21module.exports = async (to, content) => {
22
23 // The from and to addresses for the email that is about to be sent.
24 const contacts = {
25 from: process.env.MAIL_USER,
26 to
27 }
28
29 // Combining the content and contacts into a single object that can
30 // be passed to Nodemailer.
31 const email = Object.assign({}, content, contacts)
32
33 // This file is imported into the controller as 'sendEmail'. Because
34 // 'transporter.sendMail()' below returns a promise we can write code like this
35 // in the contoller when we are using the sendEmail() function.
36 //
37 // sendEmail()
38 // .then(() => doSomethingElse())
39 //
40 // If you are running into errors getting Nodemailer working, wrap the following
41 // line in a try/catch. Most likely is not loading the credentials properly in
42 // the .env file or failing to allow unsafe apps in your gmail settings.
43 await transporter.sendMail(email)
44
45}
فایل email.templates.js
ما تنها یک نوع ایمیل در این اپلیکیشن ارسال میکنیم که ایمیل تأییدیه است. در نتیجه باید این قالب ایمیل را در فایلی مانند email.controller.js یا email.send.js قرار دهیم؛ اما این کار موجب میشود که بخشی از کارکرد اپلیکیشن در آن فایلها قرار گیرد و همان طور که قبلاً اشاره کردیم email.templates.js هیچ ارتباطی با منطق اپلیکیشن ندارد. در نتیجه باید یک فایل اختصاصی برای قالب ایمیل به نام email.templates.js بسازیم. بنابراین اگر در ادامه بخواهیم یک گردش کاری برای ایمیلهای «تأیید نشده»، «تبریک تولد» یا هر چیز دیگر بسازیم، این فایل به سادگی قابل بسط خواهد بود.
1const { CLIENT_ORIGIN } = require('../config')
2
3// This file is exporting an Object with a single key/value pair.
4// However, because this is not a part of the logic of the application
5// it makes sense to abstract it to another file. Plus, it is now easily
6// extensible if the application needs to send different email templates
7// (eg. unsubscribe) in the future.
8module.exports = {
9
10 confirm: id => ({
11 subject: 'React Confirm Email',
12 html: `
13 <a href='${CLIENT_ORIGIN}/confirm/${id}'>
14 click to confirm email
15 </a>
16 `,
17 text: `Copy and paste this link: ${CLIENT_ORIGIN}/confirm/${id}`
18 })
19
20}
سخن پایانی
همان طور که در این راهنما دیدیم یک گردش کاری ساده مانند گردآوری آدرس ایمیل به یک اپلیکیشن کامل نیاز دارد. با جداسازی بخشهای کلاینت و سرور در بخشهای مجزا و قابل استفاده مجدد، این امکان را فراهم میکنیم که در صورتی که لازم باشد کارکردهای مختلفی در آینده به اپلیکیشن اضافه شوند، به راحتی آن را توسعه دهیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزش های برنامه نویسی
- آموزش مقدماتی فریمورک React Native برای طراحی نرم افزارهای اندروید و iOS با زبان جاوا اسکریپت
- مجموعه آموزش های طراحی و برنامه نویسی وب
- آموزش React.js در کمتر از ۵ دقیقه — از صفر تا صد
- راهنمای جامع React (بخش اول) — از صفر تا صد
- واکشی (Fetch) کردن داده ها در اپلیکیشن های React — به زبان ساده
==
سلام و وقت بخیر؛
برای بازیابی نشانی ایمیل، میبایست گزینه «فراموش کردن گذرواژه» یا عبارتهای مشابه را در پرووایدرهای مختلف کلیک کنید. به این ترتیب با استفاده از روشهای مختلفی که برای راستیآزمایی ادعای مالکیت شما روی نشانی مورد نظر وجود دارد، میتوانید نشانی ایمیل خود را بازیابی نمایید.
از توجه شما متشکرم.
سلام عرض خسته نباشید خدمت شما هز چی کوشیش می کنم ایمیل من باز نمیشه چند مرحله پیش میرم اما باز نمیشه چون قبلا به گوشی دگه من بوده که دگه قابل تعمیر نمیشه و ایمیل من باز نمیشه میشود یک کاری برای من بکنید